Skip to content

feat: filters and partial cloning: initial support#2375

Open
staehle wants to merge 3 commits intoGitoxideLabs:mainfrom
staehle:user/jstaehle/partial-clone-filters-oss
Open

feat: filters and partial cloning: initial support#2375
staehle wants to merge 3 commits intoGitoxideLabs:mainfrom
staehle:user/jstaehle/partial-clone-filters-oss

Conversation

@staehle
Copy link

@staehle staehle commented Jan 13, 2026

Resolves #1046

Hi @Byron! I'm Jake Staehle and I work at Roku here with @cesfahani. We've had an internal version of this for our internal tools for quite some time, but I recently updated our gitoxide version to 0.49 and had to refactor this code myself for all the changes since 0.40 -- I would very much rather not do that again, so it's upstreaming time :)

Introduce gix::remote::fetch::ObjectFilter (currently blob filters only) and plumb it through clone/fetch all the way into the fetch protocol, returning a clear error if the remote doesn’t advertise filter capability.

Also persist the partial-clone configuration on clone (remote.<name>.partialclonefilter, remote.<name>.promisor, extensions.partialclone) so the repository is marked as a promisor/partial clone in the same way as Git.

On the CLI/plumbing side, add --filter <spec> to gix clone, and allow fetch arguments to be either refspecs or raw object IDs (treated as additional wants).

Includes tests for filter parsing and for persisting partial-clone settings during clone, as well as a full round-trip test creating a blobless bare clone using gix, creating (using real git) a worktree from that, and then using gix verify that --filter=blob:none is working!

I did create a new repo https://github.com/staehle/gitoxide-testing for that last test to work properly, with a small set of known commits, tags, and blob objects. If you think this is useful, I'd be glad to transfer the repo to you / the GitoxideLabs group!

Let me know what you think! Thanks!

Credit:

Copilot AI review requested due to automatic review settings January 13, 2026 17:41
@staehle staehle force-pushed the user/jstaehle/partial-clone-filters-oss branch 2 times, most recently from 3f30421 to ddd9fd1 Compare January 13, 2026 18:26
Copy link

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

self.shallow,
negotiate::make_refmapping_ignore_predicate(self.tags, self.ref_map),
)
if self.additional_wants.is_empty() {
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

The condition is inverted. When additional_wants is empty, the code calls negotiate::add_wants() but then still iterates over additional_wants (lines 315-318), which does nothing. When additional_wants is NOT empty, it skips the normal wants logic and manually implements it (lines 323-350). This should be !self.additional_wants.is_empty() to properly handle the case when additional wants are present.

Suggested change
if self.additional_wants.is_empty() {
if !self.additional_wants.is_empty() {

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

It looks confusing so I get why copilot doesn't understand, but the condition is not inverted, and the behavior matches the surrounding design.

When additional_wants is empty, add_wants() delegates to negotiate::add_wants and then iterates self.additional_wants (which is empty), so that loop is a harmless no-op. This is the "normal fetch" path.

When additional_wants is non-empty, the code intentionally avoids negotiate::add_wants and instead builds the wants list itself (including want_ref() when supported) and then appends the additional object-id wants. This aligns with the other deliberate behavior you already saw:

  • mark_complete_and_common_ref() forces MustNegotiate when additional wants exist.
  • one_round() returns an "empty round" and intentionally skips have exchange/negotiation rounds since we want to ask for these objects directly.

So, flipping the condition to !self.additional_wants.is_empty() would make the non-empty case call negotiate::add_wants and then fall through into the manual wants logic, which would duplicate/alter behavior and be wrong.

Comment on lines +104 to +105
.map(crate::remote::fetch::ObjectFilter::to_argument_string)
.filter(|_| self.additional_wants.is_empty());
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

The filter is being suppressed when additional_wants is present (line 105). This means users cannot use object filters when fetching specific objects by ID. Unless there's a technical limitation preventing filters with additional wants, this restriction should be removed or clearly documented why filters and additional wants are mutually exclusive.

Suggested change
.map(crate::remote::fetch::ObjectFilter::to_argument_string)
.filter(|_| self.additional_wants.is_empty());
.map(crate::remote::fetch::ObjectFilter::to_argument_string);

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

Hard disagree. This is intentional -- additional_wants SHOULD ignore blob filters, since if you specifically want to checkout a commit, you NEED those blobs.

@Byron
Copy link
Member

Byron commented Jan 19, 2026

Thanks a lot for upstreaming this feature, it's much appreciated!

I hope to get to giving it the attention it deserves this weekend.

@Byron Byron self-assigned this Feb 1, 2026
staehle and others added 2 commits February 1, 2026 16:51
Introduce `gix::remote::fetch::ObjectFilter` (currently blob filters only)
and plumb it through clone/fetch all the way into the fetch protocol,
returning a clear error if the remote doesn’t advertise `filter` capability.

Also persist the partial-clone configuration on clone
(`remote.<name>.partialclonefilter`, `remote.<name>.promisor`,
`extensions.partialclone`) so the repository is marked as a
promisor/partial clone in the same way as Git.

On the CLI/plumbing side, add `--filter <spec>` to `gix clone`, and allow
fetch arguments to be either refspecs or raw object IDs
(treated as additional wants).

Includes tests for filter parsing and for persisting partial-clone settings during clone.

Creates a blobless bare clone using `gix`,
creates (using real `git`) a worktree from that,
and then using `gix` verify that `--filter=blob:none` is working!

Credit:
 - Original work by Cameron Esfahani <cesfahani@roku.com>
 - Rewritten for modern Gitoxide by Jake Staehle <jstaehle@roku.com>

[Upstream-Status: Appropriate for OSS Release]
Copy link
Member

@Byron 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'd hope to finish the review and changes today. It should be alright, but I hope I will get the negotiation logic.

Due to the lack of a server implementation this is always tricky to test, but maybe that's what the journey tests are there for.

I also wonder what it would take to port them into the gix (crate) test-suite.

Tasks

  • review no-network commit
  • address all coments

Comment on lines +38 to +39
while remote_section.remove("partialclonefilter").is_some() {}
while remote_section.remove("promisor").is_some() {}
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: Create gix-config remove all.

while remote_section.remove("partialclonefilter").is_some() {}
while remote_section.remove("promisor").is_some() {}

let partial_clone_filter = ValueName::try_from("partialclonefilter").map_err(|err| Error::ConfigValueName {
Copy link
Member

Choose a reason for hiding this comment

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

Create new config-tree keys for partialCloneFilter and promisor.

return Ok(action);
}

Ok(match action {
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: try to understand this.

}

Ok((
negotiate::Round {
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: Oh try to understand this.

Comment on lines +32 to +49
/// Errors emitted when parsing [`ObjectFilter`] values from command-line input.
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
/// The provided filter specification was empty.
#[error("filter specification must not be empty")]
Empty,
/// The provided filter specification is not supported yet.
#[error("unsupported filter specification '{0}'")]
Unsupported(String),
/// The provided blob size limit could not be parsed as an integer.
#[error("invalid blob size limit '{value}'")]
InvalidBlobLimit {
/// The string that failed to parse.
value: String,
/// The underlying parse error.
source: ParseIntError,
},
}
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: Turn this to gix-error::ValidationError right away.

…t to avoid the network.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@staehle
Copy link
Author

staehle commented Feb 9, 2026

Apologies, GitHub just did something stupid I didn't expect it to do

(I reset this branch back to the 2c60401 commit you were working on)

@staehle staehle force-pushed the user/jstaehle/partial-clone-filters-oss branch from f7ac294 to 2c60401 Compare February 9, 2026 19:31
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.

Minimal implementation of partial clones & promisor objects

3 participants