Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e75fbfa

Browse files
j-waltherByron
authored andcommitted
feat!: Allow checkouts of empty repositories
Also make turn `destination_must_be_empty` into `Option<bool>`
1 parent 575113d commit e75fbfa

7 files changed

Lines changed: 128 additions & 8 deletions

File tree

gix/src/clone/access.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,12 @@ impl PrepareFetch {
7373
impl Drop for PrepareFetch {
7474
fn drop(&mut self) {
7575
if let Some(repo) = self.repo.take() {
76-
std::fs::remove_dir_all(repo.workdir().unwrap_or_else(|| repo.path())).ok();
76+
if self.destination_was_empty {
77+
std::fs::remove_dir_all(repo.workdir().unwrap_or_else(|| repo.path())).ok();
78+
} else {
79+
// The destination held pre-existing user files; only remove the `.git` we created.
80+
std::fs::remove_dir_all(repo.path()).ok();
81+
}
7782
}
7883
}
7984
}

gix/src/clone/checkout.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,12 @@ impl PrepareCheckout {
171171
impl Drop for PrepareCheckout {
172172
fn drop(&mut self) {
173173
if let Some(repo) = self.repo.take() {
174-
std::fs::remove_dir_all(repo.workdir().unwrap_or_else(|| repo.path())).ok();
174+
if self.destination_was_empty {
175+
std::fs::remove_dir_all(repo.workdir().unwrap_or_else(|| repo.path())).ok();
176+
} else {
177+
// The destination held pre-existing user files; only remove the `.git` we created.
178+
std::fs::remove_dir_all(repo.path()).ok();
179+
}
175180
}
176181
}
177182
}

gix/src/clone/fetch/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ impl PrepareFetch {
318318
crate::clone::PrepareCheckout {
319319
repo: repo.into(),
320320
ref_name: self.ref_name.clone(),
321+
destination_was_empty: self.destination_was_empty,
321322
},
322323
fetch_outcome,
323324
))

gix/src/clone/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ pub struct PrepareFetch {
4242
/// The name of the reference to fetch. If `None`, the reference pointed to by `HEAD` will be checked out.
4343
#[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))]
4444
ref_name: Option<gix_ref::PartialName>,
45+
/// `true` iff the destination directory was empty (or did not exist) when this handle was created.
46+
/// When `false`, drop will avoid removing pre-existing user files and only clean up the `.git` we created.
47+
destination_was_empty: bool,
4548
}
4649

4750
/// The error returned by [`PrepareFetch::new()`].
@@ -102,7 +105,17 @@ impl PrepareFetch {
102105
mut create_opts: crate::create::Options,
103106
open_opts: crate::open::Options,
104107
) -> Result<Self, Error> {
105-
create_opts.destination_must_be_empty = true;
108+
if create_opts.destination_must_be_empty.is_none() {
109+
create_opts.destination_must_be_empty = Some(true);
110+
}
111+
112+
// Capture this before init_opts creates `.git`, otherwise the check below would see our own files.
113+
let destination_was_empty = match std::fs::read_dir(path) {
114+
Ok(mut entries) => entries.next().is_none(),
115+
// Non-existent (or unreadable) — init_opts will create the directory.
116+
Err(_) => true,
117+
};
118+
106119
let mut repo = crate::ThreadSafeRepository::init_opts(path, kind, create_opts, open_opts)?.to_thread_local();
107120
url.canonicalize(repo.options.current_dir_or_empty())
108121
.map_err(|err| Error::CanonicalizeUrl {
@@ -122,6 +135,7 @@ impl PrepareFetch {
122135
configure_connection: None,
123136
shallow: remote::fetch::Shallow::NoChange,
124137
ref_name: None,
138+
destination_was_empty,
125139
})
126140
}
127141
}
@@ -136,6 +150,9 @@ pub struct PrepareCheckout {
136150
pub(self) repo: Option<crate::Repository>,
137151
/// The name of the reference to check out. If `None`, the reference pointed to by `HEAD` will be checked out.
138152
pub(self) ref_name: Option<gix_ref::PartialName>,
153+
/// `true` iff the destination directory was empty (or did not exist) when the parent [`PrepareFetch`] was created.
154+
/// When `false`, drop must avoid removing pre-existing user files and only clean up the `.git` we created.
155+
pub(self) destination_was_empty: bool,
139156
}
140157

141158
// This module encapsulates functionality that works with both feature toggles. Can be combined with `fetch`

gix/src/create.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,17 @@ fn create_dir(p: &Path) -> Result<(), Error> {
110110
/// Options for use in [`into()`];
111111
#[derive(Copy, Clone, Default)]
112112
pub struct Options {
113-
/// If true, and the kind of repository to create has a worktree, then the destination directory must be empty.
113+
/// Control whether the destination directory must be empty when creating a repository with a worktree.
114114
///
115-
/// By default repos with worktree can be initialized into a non-empty repository as long as there is no `.git` directory.
116-
pub destination_must_be_empty: bool,
115+
/// - `None` (default): worktree repos may be initialized into a non-empty directory as long as no `.git`
116+
/// directory is present. [`crate::clone::PrepareFetch::new`] interprets `None` as `Some(true)` to preserve
117+
/// the historical strict-by-default behavior for clones.
118+
/// - `Some(true)`: require an empty destination directory.
119+
/// - `Some(false)`: explicitly allow initialization into a non-empty destination directory (still requires
120+
/// that no `.git` directory is present).
121+
///
122+
/// Bare repositories always require an empty destination, regardless of this option.
123+
pub destination_must_be_empty: Option<bool>,
117124
/// If set, use these filesystem capabilities to populate the respective git-config fields.
118125
/// If `None`, the directory will be probed.
119126
pub fs_capabilities: Option<gix_fs::Capabilities>,
@@ -135,7 +142,7 @@ pub fn into(
135142
let mut dot_git = directory.into();
136143
let bare = matches!(kind, Kind::Bare);
137144

138-
if bare || destination_must_be_empty {
145+
if bare || destination_must_be_empty.unwrap_or(false) {
139146
let num_entries_in_dot_git = fs::read_dir(&dot_git)
140147
.or_else(|err| {
141148
if err.kind() == std::io::ErrorKind::NotFound {

gix/tests/gix/clone.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,71 @@ mod blocking_io {
585585
Ok(())
586586
}
587587

588+
#[test]
589+
fn fetch_and_checkout_into_non_empty_directory() -> crate::Result {
590+
let tmp = gix_testtools::tempfile::TempDir::new()?;
591+
let existing_path = tmp.path().join("existing.txt");
592+
let existing_content = b"I was here before you";
593+
std::fs::write(&existing_path, existing_content)?;
594+
595+
let mut prepare = gix::clone::PrepareFetch::new(
596+
remote::repo("base").path(),
597+
tmp.path(),
598+
gix::create::Kind::WithWorktree,
599+
gix::create::Options {
600+
destination_must_be_empty: Some(false),
601+
..Default::default()
602+
},
603+
restricted(),
604+
)?;
605+
let (mut checkout, _out) =
606+
prepare.fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;
607+
let (repo, _) = checkout.main_worktree(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;
608+
609+
let index = repo.index()?;
610+
assert_eq!(index.entries().len(), 1, "All entries are known as per HEAD tree");
611+
assure_index_entries_on_disk(&index, repo.workdir().expect("non-bare"));
612+
613+
assert_eq!(std::fs::read(&existing_path)?, existing_content);
614+
Ok(())
615+
}
616+
617+
#[test]
618+
fn drop_after_failed_fetch_into_non_empty_directory_preserves_pre_existing_files() -> crate::Result {
619+
let tmp = gix_testtools::tempfile::TempDir::new()?;
620+
let existing_path = tmp.path().join("existing.txt");
621+
let existing_content = b"I was here before you";
622+
std::fs::write(&existing_path, existing_content)?;
623+
624+
let mut prepare = gix::clone::PrepareFetch::new(
625+
remote::repo("base").path(),
626+
tmp.path(),
627+
gix::create::Kind::WithWorktree,
628+
gix::create::Options {
629+
destination_must_be_empty: Some(false),
630+
..Default::default()
631+
},
632+
restricted(),
633+
)?
634+
.with_ref_name(Some("does-not-exist"))?;
635+
636+
prepare
637+
.fetch_then_checkout(gix::progress::Discard, &AtomicBool::default())
638+
.expect_err("non-existing ref must fail");
639+
drop(prepare);
640+
641+
assert_eq!(
642+
std::fs::read(&existing_path)?,
643+
existing_content,
644+
"pre-existing user files must survive a failed clone+drop"
645+
);
646+
assert!(
647+
!tmp.path().join(".git").exists(),
648+
"only the .git directory we created should have been cleaned up"
649+
);
650+
Ok(())
651+
}
652+
588653
#[test]
589654
fn fetch_and_checkout_specific_ref() -> crate::Result {
590655
let tmp = gix_testtools::tempfile::TempDir::new()?;
@@ -832,6 +897,26 @@ fn clone_and_destination_must_be_empty() -> crate::Result {
832897
Ok(())
833898
}
834899

900+
#[test]
901+
fn clone_with_worktree_and_destination_must_be_empty() -> crate::Result {
902+
let tmp = gix_testtools::tempfile::TempDir::new()?;
903+
std::fs::write(tmp.path().join("file"), b"hello")?;
904+
match gix::clone::PrepareFetch::new(
905+
remote::repo("base").path(),
906+
tmp.path(),
907+
gix::create::Kind::WithWorktree,
908+
Default::default(),
909+
restricted(),
910+
) {
911+
Ok(_) => unreachable!("this should fail as the directory isn't empty"),
912+
Err(err) => assert!(
913+
err.to_string()
914+
.starts_with("Refusing to initialize the non-empty directory as ")
915+
),
916+
}
917+
Ok(())
918+
}
919+
835920
#[test]
836921
fn clone_bare_into_empty_directory_and_early_drop() -> crate::Result {
837922
let tmp = gix_testtools::tempfile::TempDir::new()?;

gix/tests/gix/init.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ mod non_bare {
172172
tmp.path(),
173173
gix::create::Kind::WithWorktree,
174174
gix::create::Options {
175-
destination_must_be_empty: true,
175+
destination_must_be_empty: Some(true),
176176
..Default::default()
177177
},
178178
gix::open::Options::isolated(),

0 commit comments

Comments
 (0)