Skip to content

Allow checkouts of repositories into non-empty directories#2508

Open
Jan Walther (j-walther) wants to merge 7 commits intoGitoxideLabs:mainfrom
j-walther:feat/allow-empty-dir
Open

Allow checkouts of repositories into non-empty directories#2508
Jan Walther (j-walther) wants to merge 7 commits intoGitoxideLabs:mainfrom
j-walther:feat/allow-empty-dir

Conversation

@j-walther
Copy link
Copy Markdown
Contributor

This line effectively prevented checking out in non-empty directories in all cases.

Copy link
Copy Markdown
Member

@Byron Sebastian Thiel (Byron) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, but I think this needs test coverage.
I would also be afraid that it now allows writing non-empty directories, wiping data in the process.

@j-walther
Copy link
Copy Markdown
Contributor Author

I would also be afraid that it now allows writing non-empty directories, wiping data in the process.
The current state is that it's effectively impossible to check-out into a non-empty directory making that user-facing flag effectively superfluous.

What would you expect tests for this to look like? Checking out into a non empty directory with the flag both set and not set? The latter would just be a regular checkout.

@Byron
Copy link
Copy Markdown
Member

There needs to be a test that fails if the change of this PR isn't present.
Also, there must be a test that shows non-empty directories cannot be checked out into to address

I would also be afraid that it now allows writing non-empty directories, wiping data in the process.

It might already exist as well, in which case you can point it out to me.

@j-walther
Copy link
Copy Markdown
Contributor Author

Jan Walther (j-walther) commented Apr 13, 2026

I've added the tests. What I think should be done is to change the Default implementation to set destination_must_be_empty to true by default.

Copy link
Copy Markdown
Member

@Byron Sebastian Thiel (Byron) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I understand the premise. The reason that it won't allow cloning into a non-empty directory is that it will brutally overwrite everything. Git also rejects this, AFAIK.

❯ git clone https://github.com/GitoxideLabs/gitoxide ./src/
fatal: destination path './src' already exists and is not an empty directory.

The PR title is "Allow checkouts of empty repositories". Thus a test should show that empty repositories couldn't be checked out before, and after the fix they can.

I've added the tests. What I think should be done is to change the Default implementation to set destination_must_be_empty to true by default.

I agree, and wonder why I didn't that before.

One problem remains: I don't understand what the PR is trying to fix.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts gix::clone::PrepareFetch::new() to honor the caller-provided gix::create::Options instead of unconditionally requiring an empty destination directory for worktree clones, aligning clone initialization behavior with the documented semantics of create::Options. It also adds regression tests covering the new default behavior and the opt-in strict mode.

Changes:

  • Stop forcing create_opts.destination_must_be_empty = true in PrepareFetch::new_inner().
  • Add a test proving fetch+checkout into a non-empty destination directory works by default (and preserves pre-existing files).
  • Add a test proving destination_must_be_empty: true still rejects non-empty destinations for worktree clones.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
gix/src/clone/mod.rs Removes the unconditional override of destination_must_be_empty, so clone respects caller-provided creation options.
gix/tests/gix/clone.rs Adds coverage for cloning into non-empty worktree destinations by default and for the strict “must be empty” option.

Comment thread gix/src/clone/mod.rs
Comment on lines 97 to 106
#[allow(clippy::result_large_err)]
fn new_inner(
mut url: gix_url::Url,
path: &std::path::Path,
kind: crate::create::Kind,
mut create_opts: crate::create::Options,
create_opts: crate::create::Options,
open_opts: crate::open::Options,
) -> Result<Self, Error> {
create_opts.destination_must_be_empty = true;
let mut repo = crate::ThreadSafeRepository::init_opts(path, kind, create_opts, open_opts)?.to_thread_local();
url.canonicalize(repo.options.current_dir_or_empty())
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title mentions "checkouts of empty repositories", but the code change (and new tests) are about allowing clone/fetch+checkout into a non-empty destination directory by default (unless destination_must_be_empty is set). Consider updating the PR title (or description) to match the actual behavior change to avoid confusion in release notes/changelog context.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@j-walther
Copy link
Copy Markdown
Contributor Author

One problem remains: I don't understand what the PR is trying to fix.

Before my changes every code path set the destination_must_be_empty flag to true in a part of the code the user couldn't influence so you could never check out into a non-empty directory. This made the flag essentially meaningless since it'd effectively always be true. This PR removes the line that forces it to always be true so the user can actually opt into checking out into non-empty directories.

@j-walther
Copy link
Copy Markdown
Contributor Author

I've updated the tests to better reflect the change and made it so destination_must_be_empty is true by default.

@j-walther Jan Walther (j-walther) changed the title Allow checkouts of empty repositories Allow checkouts of repositories into non-empty directories Apr 13, 2026
Comment thread gix/src/lib.rs Outdated
destination_must_be_empty: false,
..Default::default()
},
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restores previous behaviour which was also enforced by the init_into_non_empty_directory_is_allowed_by_default test.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread gix/src/clone/mod.rs Outdated
Comment on lines 102 to 104
create_opts: crate::create::Options,
open_opts: crate::open::Options,
) -> Result<Self, Error> {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that PrepareFetch::new_inner() no longer forces destination_must_be_empty = true, callers can legitimately pass destination_must_be_empty: false. However, PrepareFetch/PrepareCheckout currently auto-clean up in Drop by calling remove_dir_all(repo.workdir()…), which will delete the entire destination directory—including pre-existing user files—if clone/fetch/checkout fails or the handle is dropped without persist(). Please adjust cleanup to be safe for non-empty destinations (e.g., track whether the destination was newly created/empty and only remove_dir_all in that case, otherwise only remove the created .git dir or disable auto-cleanup and require explicit cleanup by the caller).

Suggested change
create_opts: crate::create::Options,
open_opts: crate::open::Options,
) -> Result<Self, Error> {
mut create_opts: crate::create::Options,
open_opts: crate::open::Options,
) -> Result<Self, Error> {
create_opts.destination_must_be_empty = true;

Copilot uses AI. Check for mistakes.
Comment thread gix/src/create.rs Outdated
Comment on lines +113 to +127
@@ -119,6 +119,15 @@ pub struct Options {
pub fs_capabilities: Option<gix_fs::Capabilities>,
}

impl Default for Options {
fn default() -> Self {
Options {
destination_must_be_empty: true,
fs_capabilities: None,
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create::Options's field docs say worktree repos can be initialized into a non-empty directory “by default” (as long as no .git exists), but Default for Options now sets destination_must_be_empty: true, which makes the default behavior the opposite. Please align documentation and behavior (either change the default back to false and have clone code set it to true, or update the docs to state the default is strict and that gix::init()/CLI init explicitly override it to false).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment thread gix/src/create.rs Outdated
Comment thread gix/src/clone/mod.rs
Comment on lines +105 to +107
if create_opts.destination_must_be_empty.is_none() {
create_opts.destination_must_be_empty = Some(true);
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing destination_must_be_empty = Some(false) makes it possible to clone/checkout into an existing directory, but PrepareFetch/PrepareCheckout currently remove_dir_all() the repository workdir on drop. If a fetch/checkout fails (or the handle is dropped early), this will delete the entire destination directory including pre-existing user files. Consider changing cleanup to only remove what was created (e.g. just the .git dir for worktree repos, or track whether the destination dir existed/was empty and only then delete the whole directory).

Copilot uses AI. Check for mistakes.
Comment thread gix/tests/gix/clone.rs
Comment on lines +555 to +573
fn fetch_and_checkout_into_non_empty_directory() -> crate::Result {
let tmp = gix_testtools::tempfile::TempDir::new()?;
let existing_path = tmp.path().join("existing.txt");
let existing_content = b"I was here before you";
std::fs::write(&existing_path, existing_content)?;

let mut prepare = gix::clone::PrepareFetch::new(
remote::repo("base").path(),
tmp.path(),
gix::create::Kind::WithWorktree,
gix::create::Options {
destination_must_be_empty: Some(false),
..Default::default()
},
restricted(),
)?;
let (mut checkout, _out) =
prepare.fetch_then_checkout(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;
let (repo, _) = checkout.main_worktree(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new behavior (checkout into a non-empty destination) would benefit from a regression test that covers the failure/rollback path: if fetch_then_checkout() or main_worktree() errors, dropping the prepare/checkout handle should not delete or modify pre-existing files in the destination directory.

Copilot uses AI. Check for mistakes.
Comment thread gix/src/create.rs
Comment on lines 113 to 115
/// If true, and the kind of repository to create has a worktree, then the destination directory must be empty.
///
/// By default repos with worktree can be initialized into a non-empty repository as long as there is no `.git` directory.
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destination_must_be_empty is now an Option<bool>, but the field docs still describe only the true/false cases. Please update the doc comment to explain the None behavior (what default is assumed, and how callers should use Some(true) vs Some(false)), especially since this is a public API surface.

Suggested change
/// If true, and the kind of repository to create has a worktree, then the destination directory must be empty.
///
/// By default repos with worktree can be initialized into a non-empty repository as long as there is no `.git` directory.
/// Control whether the destination directory must be empty when creating a repository with a worktree.
///
/// If this is `None`, the default is used: repositories with a worktree may be initialized into a non-empty
/// directory as long as there is no `.git` directory already present.
///
/// Use `Some(true)` to explicitly require an empty destination directory for repositories with a worktree.
/// Use `Some(false)` to explicitly allow initialization into a non-empty destination directory, subject to the
/// absence of a `.git` directory.

Copilot uses AI. Check for mistakes.
Comment thread gix/src/create.rs
///
/// By default repos with worktree can be initialized into a non-empty repository as long as there is no `.git` directory.
pub destination_must_be_empty: bool,
pub destination_must_be_empty: Option<bool>,
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing destination_must_be_empty from bool to Option<bool> is a breaking change to the public gix::create::Options API. If avoiding breakage is desired, consider an alternative that preserves the existing field (e.g. a new field or dedicated clone option) or ensure this is called out appropriately for downstream users (versioning/release notes).

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Matthew Dowell (Mbd06b) added a commit to ethosengine/brit that referenced this pull request Apr 25, 2026
## Summary

Closes 9 compat-mode rows in tests/journey/parity/branch.sh, lands three
reusable foundations consumed by future sprints (commit, push, tag, merge),
and folds in 6 upstream syncs that arrived during the sprint.

## Closed parity rows (all bytes-mode, steward-verified, on-disk state asserted)

- `-v` / `--verbose` — column-aligned name + SHA + subject
- `-vv` — adds `[upstream: ahead N, behind N]` tracking annotation
- `--abbrev=N` / `--no-abbrev` — SHA width override
- `--column=always` — column-major packing
- `-u` / `--set-upstream-to` — writes branch.<n>.{remote,merge}
- `--unset-upstream` — clears them; emits `fatal: branch '<n>' has no upstream information` + exit 128 when absent
- `--track` (create-side) — writes upstream config after ref creation
- `--edit-description` — spawns editor, writes branch.<n>.description

Plus three on-disk-state guard rows (chained gix → gix invocations) so
bytes-mode parity tests can no longer mask missing-side-effect regressions
in branch-config writers.

## Foundations introduced

- gix::Repository::{set_branch_upstream, unset_branch_upstream, set_branch_description}
  with $GIT_DIR/config persistence via gix_config::File::write_to_filter
  on Source::Local sections (matches gix/src/clone/fetch/util.rs::setup_branch_config).
- Branch::DESCRIPTION key constant + SnapshotMut::clear_subsection_value helper.
- gitoxide-core::shared::columns::write_columns — column-major packer (reusable
  for status --column).
- gitoxide-core::shared::editor::edit_file — $GIT_EDITOR/core.editor/$VISUAL/$EDITOR/vi
  resolver + shell-spawn (reusable for commit -e, tag --edit, merge --edit).

## Upstream syncs folded in

- merge: pull upstream gix-main into gix-brit (security hardening + fuzz expansion)
- merge: pull GitoxideLabs#2508 — allow checkouts into non-empty directories
- merge: pull GitoxideLabs#2375 — filters and partial cloning: initial support
- merge: pull GitoxideLabs#2503 — IOUC (UNTR extension) dirwalk speedup
- merge: pull GitoxideLabs#2457 — gix-blame: Incremental
- merge: pull GitoxideLabs#2465 — barebones server-side upload-pack

## Adjustments

- steward agent: surface idiomatic Rust as an explicit design priority,
  reframing vendor/git as the *what* (flag surface, exit codes, output bytes)
  while gitoxide idioms anchor *how*.
- Post-merge cleanup: dropped duplicate clone --filter stub (superseded by GitoxideLabs#2375)
  and pr-2457's orphaned log helpers (our log_all already handles paths).

Plan: etc/plan/foundations-sprint-branch-render-and-config.md
Pre-squash backup tag: pre-squash-foundations

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants