Skip to content

Commit 1a75f72

Browse files
Merge pull request #11434 from gitbutlerapp/banishing-gix
Use git to clone projects
2 parents e7a68ba + baec680 commit 1a75f72

File tree

6 files changed

+154
-22
lines changed

6 files changed

+154
-22
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/but-api/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ legacy = [
3737
"dep:gitbutler-operating-modes",
3838
"dep:gitbutler-sync",
3939
"dep:gitbutler-oplog",
40+
"dep:gitbutler-git",
4041
"dep:but-action",
4142
"dep:but-hunk-assignment",
4243
"dep:but-hunk-dependency",
@@ -96,6 +97,7 @@ gitbutler-edit-mode = {workspace = true, optional = true}
9697
gitbutler-operating-modes = {workspace = true, optional = true}
9798
gitbutler-sync = {workspace = true, optional = true}
9899
gitbutler-oplog = {workspace = true, optional = true}
100+
gitbutler-git = {workspace = true, optional = true}
99101

100102
tauri = { version = "^2.9.3", features = ["unstable"], optional = true } # For the #[tauri::command(async)] macro only
101103

crates/but-api/src/legacy/repo.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{path::PathBuf, sync::atomic::AtomicBool};
1+
use std::path::PathBuf;
22

33
use anyhow::{Context as _, Result};
44
use but_api_macros::api_cmd_tauri;
@@ -12,6 +12,7 @@ use gitbutler_repo::{
1212
FileInfo, RepoCommands,
1313
hooks::{HookResult, MessageHookResult},
1414
};
15+
use gitbutler_repo_actions::askpass;
1516
use tracing::instrument;
1617

1718
#[api_cmd_tauri]
@@ -38,13 +39,29 @@ pub fn check_signing_settings(project_id: ProjectId) -> Result<bool> {
3839
#[api_cmd_tauri]
3940
#[instrument(err(Debug))]
4041
pub fn git_clone_repository(repository_url: String, target_dir: PathBuf) -> Result<()> {
41-
let should_interrupt = AtomicBool::new(false);
42+
let url_for_context = repository_url.clone();
43+
44+
std::thread::spawn(move || {
45+
tokio::runtime::Runtime::new()
46+
.unwrap()
47+
.block_on(gitbutler_git::clone(
48+
&repository_url,
49+
&target_dir,
50+
gitbutler_git::tokio::TokioExecutor,
51+
handle_git_prompt_clone,
52+
url_for_context,
53+
))
54+
})
55+
.join()
56+
.unwrap()
57+
.map_err(|e| anyhow::anyhow!("{e}"))
58+
}
4259

43-
gix::prepare_clone(repository_url.as_str(), &target_dir)?
44-
.fetch_then_checkout(gix::progress::Discard, &should_interrupt)
45-
.map(|(checkout, _outcome)| checkout)?
46-
.main_worktree(gix::progress::Discard, &should_interrupt)?;
47-
Ok(())
60+
async fn handle_git_prompt_clone(prompt: String, url: String) -> Option<String> {
61+
tracing::info!("received prompt for clone of {url}: {prompt:?}");
62+
askpass::get_broker()
63+
.submit_prompt(prompt, askpass::Context::Clone { url })
64+
.await
4865
}
4966

5067
#[api_cmd_tauri]

crates/gitbutler-git/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,5 @@ pub use self::executor::tokio;
2828
pub use self::{
2929
error::Error,
3030
refspec::{Error as RefSpecError, RefSpec},
31-
repository::{fetch, push, sign_commit},
31+
repository::{clone, fetch, push, sign_commit},
3232
};

crates/gitbutler-git/src/repository.rs

Lines changed: 125 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{collections::HashMap, path::Path, time::Duration};
22

33
use futures::{FutureExt, select};
4+
use gix::{bstr::ByteSlice, config::tree::Key};
45
use rand::Rng;
56

67
use super::executor::{AskpassServer, GitExecutor, Pid, Socket};
@@ -62,9 +63,16 @@ pub type Error<E> = RepositoryError<
6263
<<<E as GitExecutor>::ServerHandle as AskpassServer>::SocketHandle as Socket>::Error,
6364
>;
6465

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+
6573
#[cold]
6674
async fn execute_with_auth_harness<P, F, Fut, E, Extra>(
67-
repo_path: P,
75+
harness_env: HarnessEnv<P>,
6876
executor: &E,
6977
args: &[&str],
7078
envs: Option<HashMap<String, String>>,
@@ -180,7 +188,7 @@ where
180188
.or_else(|| std::env::var("GIT_SSH").ok())
181189
{
182190
Some(v) => v,
183-
None => get_core_sshcommand(&repo_path)
191+
None => get_core_sshcommand(&harness_env)
184192
.ok()
185193
.flatten()
186194
.unwrap_or_else(|| "ssh".into()),
@@ -215,10 +223,13 @@ where
215223
),
216224
);
217225

226+
let cwd = match harness_env {
227+
HarnessEnv::Repo(p) | HarnessEnv::Global(p) => p,
228+
};
218229
let mut child_process = core::pin::pin! {
219230
async {
220231
executor
221-
.execute(args, repo_path, Some(envs))
232+
.execute(args, cwd, Some(envs))
222233
.await
223234
.map_err(Error::<E>::Exec)
224235
}.fuse()
@@ -324,8 +335,15 @@ where
324335
args.push(remote);
325336
args.push(&refspec);
326337

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?;
329347

330348
if status == 0 {
331349
Ok(())
@@ -399,8 +417,15 @@ where
399417
args.push(opt.as_str());
400418
}
401419

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?;
404429

405430
if status == 0 {
406431
return Ok(stderr);
@@ -498,8 +523,15 @@ where
498523
"--allow-empty",
499524
"--allow-empty-message",
500525
];
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?;
503535
if status != 0 {
504536
return Err(Error::<E>::Failed {
505537
status,
@@ -549,9 +581,88 @@ where
549581
Ok(commit_hash)
550582
}
551583

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+
}
557668
}

crates/gitbutler-repo-actions/src/askpass.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub enum Context {
4747
Push { branch_id: Option<StackId> },
4848
Fetch { action: String },
4949
SignedCommit { branch_id: Option<StackId> },
50+
Clone { url: String },
5051
}
5152

5253
#[derive(Clone)]

0 commit comments

Comments
 (0)