mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-06 08:40:24 +00:00

Closes https://github.com/astral-sh/uv/issues/1775 Closes https://github.com/astral-sh/uv/issues/1452 Closes https://github.com/astral-sh/uv/issues/1514 Follows https://github.com/astral-sh/uv/pull/1717 libgit2 does not support host names with extra identifiers during SSH lookup (e.g. [`github.com-some_identifier`]( https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys#using-multiple-repositories-on-one-server)) so we use the `git` command instead for fetching. This is required for `pip` parity. See the [Cargo documentation](https://doc.rust-lang.org/nightly/cargo/reference/config.html#netgit-fetch-with-cli) for more details on using the `git` CLI instead of libgit2. We may want to try to use libgit2 first in the future, as it is more performant (#1786). We now support authentication with: ``` git+ssh://git@<hostname>/... git+ssh://git@<hostname>-<identifier>/... ``` Tested with a deploy key e.g. ``` cargo run -- \ pip install uv-private-pypackage@git+ssh://git@github.com-test-uv-private-pypackage/astral-test/uv-private-pypackage.git \ --reinstall --no-cache -v ``` and ``` cargo run -- \ pip install uv-private-pypackage@git+ssh://git@github.com/astral-test/uv-private-pypackage.git \ --reinstall --no-cache -v ``` with a ssh config like ``` Host github.com Hostname github.com IdentityFile=/Users/mz/.ssh/id_ed25519 Host github.com-test-uv-private-pypackage Hostname github.com IdentityFile=/Users/mz/.ssh/id_ed25519 ``` It seems quite hard to add test coverage for this to the test suite, as we'd need to add the SSH key and I don't know how to isolate that from affecting other developer's machines.
152 lines
4.8 KiB
Rust
152 lines
4.8 KiB
Rust
//! Git support is derived from Cargo's implementation.
|
||
//! Cargo is dual-licensed under either Apache 2.0 or MIT, at the user's choice.
|
||
//! Source: <https://github.com/rust-lang/cargo/blob/23eb492cf920ce051abfc56bbaf838514dc8365c/src/cargo/sources/git/source.rs>
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use anyhow::Result;
|
||
use reqwest::Client;
|
||
use tracing::debug;
|
||
use url::Url;
|
||
|
||
use cache_key::{digest, RepositoryUrl};
|
||
|
||
use crate::git::GitRemote;
|
||
use crate::{FetchStrategy, GitSha, GitUrl};
|
||
|
||
/// A remote Git source that can be checked out locally.
|
||
pub struct GitSource {
|
||
/// The Git reference from the manifest file.
|
||
git: GitUrl,
|
||
/// The HTTP client to use for fetching.
|
||
client: Client,
|
||
/// The fetch strategy to use when cloning.
|
||
strategy: FetchStrategy,
|
||
/// The path to the Git source database.
|
||
cache: PathBuf,
|
||
/// The reporter to use for this source.
|
||
reporter: Option<Box<dyn Reporter>>,
|
||
}
|
||
|
||
impl GitSource {
|
||
/// Initialize a new Git source.
|
||
pub fn new(git: GitUrl, cache: impl Into<PathBuf>) -> Self {
|
||
Self {
|
||
git,
|
||
client: Client::new(),
|
||
strategy: FetchStrategy::Cli,
|
||
cache: cache.into(),
|
||
reporter: None,
|
||
}
|
||
}
|
||
|
||
/// Set the [`Reporter`] to use for this `GIt` source.
|
||
#[must_use]
|
||
pub fn with_reporter(self, reporter: impl Reporter + 'static) -> Self {
|
||
Self {
|
||
reporter: Some(Box::new(reporter)),
|
||
..self
|
||
}
|
||
}
|
||
|
||
/// Fetch the underlying Git repository at the given revision.
|
||
pub fn fetch(self) -> Result<Fetch> {
|
||
// The path to the repo, within the Git database.
|
||
let ident = digest(&RepositoryUrl::new(&self.git.repository));
|
||
let db_path = self.cache.join("db").join(&ident);
|
||
|
||
let remote = GitRemote::new(&self.git.repository);
|
||
let (db, actual_rev, task) = match (self.git.precise, remote.db_at(&db_path).ok()) {
|
||
// If we have a locked revision, and we have a preexisting database
|
||
// which has that revision, then no update needs to happen.
|
||
(Some(rev), Some(db)) if db.contains(rev.into()) => (db, rev, None),
|
||
|
||
// ... otherwise we use this state to update the git database. Note
|
||
// that we still check for being offline here, for example in the
|
||
// situation that we have a locked revision but the database
|
||
// doesn't have it.
|
||
(locked_rev, db) => {
|
||
debug!("Updating git source `{:?}`", self.git.repository);
|
||
|
||
// Report the checkout operation to the reporter.
|
||
let task = self.reporter.as_ref().map(|reporter| {
|
||
reporter.on_checkout_start(remote.url(), self.git.reference.as_str())
|
||
});
|
||
|
||
let (db, actual_rev) = remote.checkout(
|
||
&db_path,
|
||
db,
|
||
&self.git.reference,
|
||
locked_rev.map(git2::Oid::from),
|
||
self.strategy,
|
||
&self.client,
|
||
)?;
|
||
|
||
(db, GitSha::from(actual_rev), task)
|
||
}
|
||
};
|
||
|
||
// Don’t use the full hash, in order to contribute less to reaching the
|
||
// path length limit on Windows.
|
||
let short_id = db.to_short_id(actual_rev.into())?;
|
||
|
||
// Check out `actual_rev` from the database to a scoped location on the
|
||
// filesystem. This will use hard links and such to ideally make the
|
||
// checkout operation here pretty fast.
|
||
let checkout_path = self
|
||
.cache
|
||
.join("checkouts")
|
||
.join(&ident)
|
||
.join(short_id.as_str());
|
||
db.copy_to(
|
||
actual_rev.into(),
|
||
&checkout_path,
|
||
self.strategy,
|
||
&self.client,
|
||
)?;
|
||
|
||
// Report the checkout operation to the reporter.
|
||
if let Some(task) = task {
|
||
if let Some(reporter) = self.reporter.as_ref() {
|
||
reporter.on_checkout_complete(remote.url(), short_id.as_str(), task);
|
||
}
|
||
}
|
||
|
||
Ok(Fetch {
|
||
git: self.git.with_precise(actual_rev),
|
||
path: checkout_path,
|
||
})
|
||
}
|
||
}
|
||
|
||
pub struct Fetch {
|
||
/// The [`GitUrl`] reference that was fetched.
|
||
git: GitUrl,
|
||
/// The path to the checked out repository.
|
||
path: PathBuf,
|
||
}
|
||
|
||
impl Fetch {
|
||
pub fn git(&self) -> &GitUrl {
|
||
&self.git
|
||
}
|
||
|
||
pub fn path(&self) -> &Path {
|
||
&self.path
|
||
}
|
||
|
||
pub fn into_git(self) -> GitUrl {
|
||
self.git
|
||
}
|
||
|
||
pub fn into_path(self) -> PathBuf {
|
||
self.path
|
||
}
|
||
}
|
||
|
||
pub trait Reporter: Send + Sync {
|
||
/// Callback to invoke when a repository checkout begins.
|
||
fn on_checkout_start(&self, url: &Url, rev: &str) -> usize;
|
||
|
||
/// Callback to invoke when a repository checkout completes.
|
||
fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize);
|
||
}
|