Skip to content

Git dependency subfolders via ?path= - #2497

Merged
bfiete merged 1 commit into
beefytech:masterfrom
mdsitton:feature/git-dependency-subpaths
Sep 7, 2026
Merged

Git dependency subfolders via ?path=#2497
bfiete merged 1 commit into
beefytech:masterfrom
mdsitton:feature/git-dependency-subpaths

Conversation

@mdsitton

@mdsitton mdsitton commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

I've been working on issue #2453 on and off since i made the issue, but finally decided to get it polished up this weekend.

Since I didn't get any specific feedback the preferred approach, I've kept with the overall design i outlined in the issue.

This pull request introduces a Version 2 BeefManaged.toml file with a very similar format to the original but with the addition of a [[Projects]] array added. Which allows multiple projects within a given cloned repo to be tracked for a beef project.

This implementation will only clone any given repo once for a given commit hash, if 2 dependencies in the same repo resolve to different commit hashes there will still be 2 copies of the repo.

Setup remains project-local. Previously, the project root was always the repository root. With this change, setup runs from the selected project’s Setup/ directory. Existing root-level dependencies retain their behavior; selecting a subfolder does not automatically run the repository-root setup.

This is the new behavior:

  BeefManaged/<commit-hash>/
    BeefManaged.toml
    Setup/             <-- runs when the root project is selected
    libraryA/
      Setup/           <-- runs when libraryA is selected
    libraryB/
      Setup/           <-- runs when libraryB is selected

Previously, Beef ran one Setup/ per managed Git dependency, located at the repository root:

  BeefManaged/<commit-hash>/
    BeefProj.toml
    Setup/

Here is the toml produced for the case above where the root project, libraryA, and libraryB are all initialized:

FileVersion = 2
Version = "v1.1.0"
GitURL = "https://github.com/user/proj.git"
GitTag = "v1.1.0"
GitHash = "0e86f98bc7436544c68bf9ff6dbe8103606912a5"
Setup = true

[[Projects]]
Path = "."
Setup = true

[[Projects]]
Path = "libraryA"
Setup = true

[[Projects]]
Path = "libraryB"
Setup = true

The top-level Setup key mirrors the Path = "." entry for compatibility: any Beef build prior to this PR reads only that key and never checks FileVersion. This allows root-only libraries to continue loading when a managed cache is shared between such a build and one with this change.

If no workspaces have initialized the root level project in this cached clone no top-level Setup key will exist:

FileVersion = 2
Version = "v1.1.0"
GitURL = "https://github.com/user/proj.git"
GitTag = "v1.1.0"
GitHash = "0e86f98bc7436544c68bf9ff6dbe8103606912a5"

[[Projects]]
Path = "libraryA"
Setup = true

[[Projects]]
Path = "libraryB"
Setup = true

Existing version 1 caches remain readable and are not rewritten by a root-only dependency. The first time a subfolder is initialized for that clone, the root's Setup key is copied to a Path = "." entry and the file is rewritten as version 2.

It's worth explicitly noting here that Setup = true on an entry means the project initialized successfully (whether or not it had a Setup folder), false means its setup attempted to run but did not complete successfully, and a missing entry means it has not been initialized yet.

The syntax for selecting projects is based on a url query parameter that is parsed by beef, as mentioned in the issue.

https://github.com/user/proj.git?path=/libraryA
https://github.com/user/proj.git?path=/libraryB

This design is based on what unity does for it's git based package manager which i've found over the years works very well in my projects.

The workspace lock file will store the full dependency URL including ?path=, so two projects from one repository
are locked independently:

FileVersion = 1

[Locks.LibraryA.Git]
URL = "https://github.com/user/proj.git?path=/libraryA"
Tag = "v1.1.0"
Hash = "0e86f98bc7436544c68bf9ff6dbe8103606912a5"

[Locks.LibraryB.Git]
URL = "https://github.com/user/proj.git?path=/libraryB"
Tag = "v1.1.0"
Hash = "0e86f98bc7436544c68bf9ff6dbe8103606912a5"

[Locks.Root.Git]
URL = "https://github.com/user/proj.git"
Tag = "v1.1.0"
Hash = "0e86f98bc7436544c68bf9ff6dbe8103606912a5"

Other changes in this PR as found by claude:

  • Plain Git URLs are now validated. A URL containing # or starting with ? is rejected with Invalid git project path. Only the path query parameter is consumed; anything else in the query string is passed through to Git unchanged.
  • A workspace lock is only used when its URL matches the dependency exactly. Changing the ?path= on a dependency re-resolves the version and writes a new lock.
  • A dependency whose selected folder or BeefProj.toml is missing records nothing in the manifest, so every load reports that specific error instead of a cached "previously failed setup". A setup that exits successfully but leaves no BeefProj.toml is recorded as failed.
  • A failed setup now fails the workspace load. Previously it only logged an error and the workspace stayed in the preparing state.
  • After the first failure, queued Git work that hasn't started yet is dropped, since the workspace load has already failed and those clones would be wasted. A setup that is already running still finishes and records its result.
  • Fixed GetWithVersion returning success without doing anything when a lock existed with a different URL. It now re-resolves the dependency.
  • A cache whose BeefManaged.toml can't be parsed or has a FileVersion other than 1 or 2 is deleted and cloned again. The reason is printed at normal verbosity, e.g. Unsupported managed metadata (FileVersion 3) at '<path>', rebuilding.
  • -cleancache and Clear Managed Cache clean the whole commit's clone, which affects every project sharing it. Each hash is rebuilt once per run even when several projects select it.
  • IDEUtils.URLDecode now returns a Result and rejects incomplete or non-hex escapes (%2, %GG, %+1). Previously a bad escape decoded to a NUL byte. The two clipboard file-URI callers in the project panel skip malformed lines; valid encoded paths are unaffected.
  • Lock and load-time failures are reported by the project loader and the affected project fails to load, instead of the package manager failing synchronously inside the workspace project loop.

Something else i wanted to bring up is for path case handling (which i also left a comment about in the code):
The manifest entry paths are matched with Environment.IsFileSystemCaseSensitive, this same approach is also used elsewhere in the IDE and corelib. This is not entirely a correct implementation in all edge cases because case sensitivity generally is a property of the file system rather than the OS. PLUS Windows 10+ can change case sensitivity per directory (https://learn.microsoft.com/en-us/windows/wsl/case-sensitivity), so a global flag is an approximation. It is usually fine but longer-term there is room for improvements across the code to handle this more correctly. This PR makes no attempt to improve these edge cases, but i did want to at least note that this is a potential issue more generally as it was something i noticed going through the code.

And finally one thing to note about compatibility with versions prior to this change:

Workspaces that use ?path= require a build with this change. Verified against a build from current master: root-only workspaces load in both directions without re-running setup thanks to the mirrored top-level Setup key. A pre-PR build given a ?path= workspace either fails to resolve the dependency (cold cache), reports "previous failed setup" (subfolder-only cache), or, if the root is also a dependency and the cache is warm, silently loads the repository root in place of the selected subfolder. During testing running prior builds without -cleancache left manifest and lock files contents unchanged, so switching back to a newer build recovers without intervention.

So overall this is quite a large rework of how the PackMan class works generally due to the additional complexity of this feature. But i do think the end result is worth having.

For transparency this work was llm assisted, i have heavily verified through testing and review of the code(and through the use of multiple models for finding edge cases and issues) prior to commit.

Allow a Git dependency URL to select a project inside the repository, for
example https://host/repo.git?path=/libraryA. Dependencies at the same commit
share one BeefManaged/<hash> clone; each selected project runs its own Setup
and records its result in a version 2 BeefManaged.toml with a [[Projects]]
entry per path. A top-level Setup key mirrors the root entry so older Beef
builds keep reading root-only libraries. Version 1 manifests remain readable
and migrate when another project in the clone is initialized.

Workspace locks store the full URL. Lock and load-time failures are reported
through the caller instead of failing inside the project loop, missing
folders record nothing so the next load repeats the same error, and caches
with unsupported metadata are rebuilt with the reason printed. URLDecode
now rejects malformed escapes and returns a Result.
@bfiete

bfiete commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

At this point I think you've spent more time thinking about and implementing Git dependencies than I have, and these all appear to be reasonable changes. I'll accept this as-is and we'll see if anyone has any input or issues.

@bfiete
bfiete merged commit 05b48fc into beefytech:master Sep 7, 2026
7 checks passed
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.

2 participants