|
1 | 1 | use std::{collections::HashMap, path::Path, time::Duration}; |
2 | 2 |
|
3 | 3 | use futures::{FutureExt, select}; |
| 4 | +use gix::{bstr::ByteSlice, config::tree::Key}; |
4 | 5 | use rand::Rng; |
5 | 6 |
|
6 | 7 | use super::executor::{AskpassServer, GitExecutor, Pid, Socket}; |
@@ -62,9 +63,16 @@ pub type Error<E> = RepositoryError< |
62 | 63 | <<<E as GitExecutor>::ServerHandle as AskpassServer>::SocketHandle as Socket>::Error, |
63 | 64 | >; |
64 | 65 |
|
| 66 | +enum HarnessEnv<P: AsRef<Path>> { |
| 67 | + /// The contained P is the repo's path |
| 68 | + Repo(P), |
| 69 | + /// The contained P is the path that the command should be executed in |
| 70 | + Global(P), |
| 71 | +} |
| 72 | + |
65 | 73 | #[cold] |
66 | 74 | async fn execute_with_auth_harness<P, F, Fut, E, Extra>( |
67 | | - repo_path: P, |
| 75 | + harness_env: HarnessEnv<P>, |
68 | 76 | executor: &E, |
69 | 77 | args: &[&str], |
70 | 78 | envs: Option<HashMap<String, String>>, |
@@ -180,7 +188,7 @@ where |
180 | 188 | .or_else(|| std::env::var("GIT_SSH").ok()) |
181 | 189 | { |
182 | 190 | Some(v) => v, |
183 | | - None => get_core_sshcommand(&repo_path) |
| 191 | + None => get_core_sshcommand(&harness_env) |
184 | 192 | .ok() |
185 | 193 | .flatten() |
186 | 194 | .unwrap_or_else(|| "ssh".into()), |
@@ -215,10 +223,13 @@ where |
215 | 223 | ), |
216 | 224 | ); |
217 | 225 |
|
| 226 | + let cwd = match harness_env { |
| 227 | + HarnessEnv::Repo(p) | HarnessEnv::Global(p) => p, |
| 228 | + }; |
218 | 229 | let mut child_process = core::pin::pin! { |
219 | 230 | async { |
220 | 231 | executor |
221 | | - .execute(args, repo_path, Some(envs)) |
| 232 | + .execute(args, cwd, Some(envs)) |
222 | 233 | .await |
223 | 234 | .map_err(Error::<E>::Exec) |
224 | 235 | }.fuse() |
@@ -324,8 +335,15 @@ where |
324 | 335 | args.push(remote); |
325 | 336 | args.push(&refspec); |
326 | 337 |
|
327 | | - let (status, stdout, stderr) = |
328 | | - execute_with_auth_harness(repo_path, &executor, &args, None, on_prompt, extra).await?; |
| 338 | + let (status, stdout, stderr) = execute_with_auth_harness( |
| 339 | + HarnessEnv::Repo(repo_path), |
| 340 | + &executor, |
| 341 | + &args, |
| 342 | + None, |
| 343 | + on_prompt, |
| 344 | + extra, |
| 345 | + ) |
| 346 | + .await?; |
329 | 347 |
|
330 | 348 | if status == 0 { |
331 | 349 | Ok(()) |
@@ -399,8 +417,15 @@ where |
399 | 417 | args.push(opt.as_str()); |
400 | 418 | } |
401 | 419 |
|
402 | | - let (status, stdout, stderr) = |
403 | | - execute_with_auth_harness(repo_path, &executor, &args, None, on_prompt, extra).await?; |
| 420 | + let (status, stdout, stderr) = execute_with_auth_harness( |
| 421 | + HarnessEnv::Repo(repo_path), |
| 422 | + &executor, |
| 423 | + &args, |
| 424 | + None, |
| 425 | + on_prompt, |
| 426 | + extra, |
| 427 | + ) |
| 428 | + .await?; |
404 | 429 |
|
405 | 430 | if status == 0 { |
406 | 431 | return Ok(stderr); |
@@ -498,8 +523,15 @@ where |
498 | 523 | "--allow-empty", |
499 | 524 | "--allow-empty-message", |
500 | 525 | ]; |
501 | | - let (status, stdout, stderr) = |
502 | | - execute_with_auth_harness(&worktree_path, &executor, &args, None, on_prompt, extra).await?; |
| 526 | + let (status, stdout, stderr) = execute_with_auth_harness( |
| 527 | + HarnessEnv::Repo(&worktree_path), |
| 528 | + &executor, |
| 529 | + &args, |
| 530 | + None, |
| 531 | + on_prompt, |
| 532 | + extra, |
| 533 | + ) |
| 534 | + .await?; |
503 | 535 | if status != 0 { |
504 | 536 | return Err(Error::<E>::Failed { |
505 | 537 | status, |
@@ -549,9 +581,88 @@ where |
549 | 581 | Ok(commit_hash) |
550 | 582 | } |
551 | 583 |
|
552 | | -fn get_core_sshcommand(cwd: impl AsRef<Path>) -> anyhow::Result<Option<String>> { |
553 | | - Ok(gix::open(cwd.as_ref())? |
554 | | - .config_snapshot() |
555 | | - .trusted_program(&gix::config::tree::Core::SSH_COMMAND) |
556 | | - .map(|program| program.to_string_lossy().into_owned())) |
| 584 | +fn get_core_sshcommand<P>(harness_env: &HarnessEnv<P>) -> anyhow::Result<Option<String>> |
| 585 | +where |
| 586 | + P: AsRef<Path>, |
| 587 | +{ |
| 588 | + match harness_env { |
| 589 | + HarnessEnv::Repo(repo_path) => Ok(gix::open(repo_path.as_ref())? |
| 590 | + .config_snapshot() |
| 591 | + .trusted_program(&gix::config::tree::Core::SSH_COMMAND) |
| 592 | + .map(|program| program.to_string_lossy().into_owned())), |
| 593 | + HarnessEnv::Global(_) => Ok(gix::config::File::from_globals()? |
| 594 | + .string(gix::config::tree::Core::SSH_COMMAND.logical_name()) |
| 595 | + .map(|program| program.to_str_lossy().into_owned())), |
| 596 | + } |
| 597 | +} |
| 598 | + |
| 599 | +/// Clones the given repository URL to the target directory. |
| 600 | +/// Any prompts for the user are passed to the asynchronous callback `on_prompt`, |
| 601 | +/// which should return the user's response or `None` if the operation should be |
| 602 | +/// aborted, in which case an `Err` value is returned from this function. |
| 603 | +/// |
| 604 | +/// Unlike fetch/push, this function always uses the Git CLI regardless of any |
| 605 | +/// backend selection, as it needs to work before a repository exists. |
| 606 | +pub async fn clone<P, F, Fut, E, Extra>( |
| 607 | + repository_url: &str, |
| 608 | + target_dir: P, |
| 609 | + executor: E, |
| 610 | + on_prompt: F, |
| 611 | + extra: Extra, |
| 612 | +) -> Result<(), crate::Error<Error<E>>> |
| 613 | +where |
| 614 | + P: AsRef<Path>, |
| 615 | + E: GitExecutor, |
| 616 | + F: FnMut(String, Extra) -> Fut, |
| 617 | + Fut: std::future::Future<Output = Option<String>>, |
| 618 | + Extra: Send + Clone, |
| 619 | +{ |
| 620 | + let target_dir = target_dir.as_ref(); |
| 621 | + |
| 622 | + // For clone, we run from the parent directory of the target |
| 623 | + let work_dir = target_dir.parent().unwrap_or(Path::new(".")); |
| 624 | + |
| 625 | + let target_dir_str = target_dir.to_string_lossy(); |
| 626 | + let args = vec!["clone", "--", repository_url, &target_dir_str]; |
| 627 | + |
| 628 | + let (status, stdout, stderr) = execute_with_auth_harness( |
| 629 | + HarnessEnv::Global(work_dir), |
| 630 | + &executor, |
| 631 | + &args, |
| 632 | + None, |
| 633 | + on_prompt, |
| 634 | + extra, |
| 635 | + ) |
| 636 | + .await?; |
| 637 | + |
| 638 | + if status == 0 { |
| 639 | + Ok(()) |
| 640 | + } else if stderr.to_lowercase().contains("permission denied") { |
| 641 | + Err(crate::Error::AuthorizationFailed(Error::<E>::Failed { |
| 642 | + status, |
| 643 | + args: args.into_iter().map(Into::into).collect(), |
| 644 | + stdout, |
| 645 | + stderr, |
| 646 | + }))? |
| 647 | + } else if stderr |
| 648 | + .to_lowercase() |
| 649 | + .contains("already exists and is not an empty directory") |
| 650 | + { |
| 651 | + Err(crate::Error::RemoteExists( |
| 652 | + target_dir.display().to_string(), |
| 653 | + Error::<E>::Failed { |
| 654 | + status, |
| 655 | + args: args.into_iter().map(Into::into).collect(), |
| 656 | + stdout, |
| 657 | + stderr, |
| 658 | + }, |
| 659 | + ))? |
| 660 | + } else { |
| 661 | + Err(Error::<E>::Failed { |
| 662 | + status, |
| 663 | + args: args.into_iter().map(Into::into).collect(), |
| 664 | + stdout, |
| 665 | + stderr, |
| 666 | + })? |
| 667 | + } |
557 | 668 | } |
0 commit comments