Skip to content

Migrate Tasks.Git.LocateRepository to multithreaded task model#1734

Open
jankratochvilcz wants to merge 8 commits into
dotnet:mainfrom
jankratochvilcz:mt/locate-repository
Open

Migrate Tasks.Git.LocateRepository to multithreaded task model#1734
jankratochvilcz wants to merge 8 commits into
dotnet:mainfrom
jankratochvilcz:mt/locate-repository

Conversation

@jankratochvilcz

@jankratochvilcz jankratochvilcz commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates Microsoft.Build.Tasks.Git.LocateRepository (and its shared abstract base RepositoryTask) to the MSBuild multithreaded (MT) task model. The MT hazard: repository discovery resolved the initial path through GitRepository.TryFindRepository, which calls Path.GetFullPath internally — making it dependent on the process current working directory (CWD), which is shared across projects in the MT model.

Approach: reject non-fully-qualified paths (attribute-only)

Rather than absolutizing the initial path via TaskEnvironment.GetAbsolutePath, this migration rejects any initial path that is not fully qualified. Repository discovery must not depend on the shared process CWD/drive; a relative, drive-relative (C:foo), or root-relative (\foo) path is rejected with the standard "missing repository" warning instead of being silently resolved against process state.

This keeps the migration attribute-only: the concrete LocateRepository carries [MSBuildMultiThreadableTask], but the base RepositoryTask needs no IMultiThreadableTask / TaskEnvironment because it no longer performs any path absolutization — it only validates.

IsPathFullyQualified polyfill

Path.IsPathFullyQualified does not exist on net472, so PathUtilities.IsPathFullyQualified provides a faithful reimplementation of the runtime's PathInternal.IsPartiallyQualified (Windows) under #if NETFRAMEWORK, delegating to the BCL API otherwise. It correctly rejects the CWD/drive-dependent forms that Path.IsPathRooted would accept (Sin 7). Covered by PathUtilitiesTests with WindowsOnly/UnixOnly-gated cases (drive-qualified, UNC, device \\?\, drive-relative, root-relative, invalid drive) plus OS-agnostic relative cases.

Base RepositoryTask

GetOrCreateRepositoryInstance() guards the initial path: if (string.IsNullOrEmpty(initialPath) || !PathUtilities.IsPathFullyQualified(initialPath)) { ReportMissingRepositoryWarning(initialPath); return null; }. The IsNullOrEmpty short-circuit preserves the pre-migration graceful behavior for empty input and avoids the polyfill's ArgumentNullException on null. Warnings keep the original input for diagnostics (Sin 2).

Concrete LocateRepository

Annotated with [MSBuildMultiThreadableTask] (Inherited=false, so each concrete class needs it). No TaskEnvironment API is required.

Behavior change (intentional)

A relative initial path that previously resolved against the process CWD is now rejected → "missing repository" warning. In the shipped targets LocateRepository receives $(MSBuildProjectDirectory) (always fully qualified), so no production scenario is affected; the change only hardens against CWD-dependent inputs.

Scope notes (from review)

  • The guard delivers current-directory / current-drive independence. Machine/user-global git-config discovery in GitEnvironment.CreateFromProcessEnvironment still reads process env vars (HOME/XDG_CONFIG_HOME/PROGRAMDATA/PATH) — this is unchanged, read-only, and identical across all projects in a build, so it does not carry the per-project shared-state hazard the migration targets.
  • The guard lives in the shared base RepositoryTask, so it also affects the sibling GetUntrackedFiles — which is migrated with the same approach in Migrate Tasks.Git.GetUntrackedFiles to multithreaded task model #1735 (and adds its own guard for its second ProjectDirectory consumption site).

Merge coordination

PathUtilities.cs, PathUtilitiesTests.cs, RepositoryTask.cs, and the test-assembly AssemblyInfo.cs are byte-identical to the sibling PR #1735, so git auto-resolves "both branches added the same content" when the second PR merges — no conflict.

Build & test

  • ./build.sh --build → 0 warnings, 0 errors (both net472 and modern TFM compile).
  • dotnet test Microsoft.Build.Tasks.Git.UnitTests -f net11.0 → 448 passed, 4 skipped (Windows-only).
  • RelativePath_IsRejected_IndependentOfProcessCwd fails if the migration is reverted (verified); AbsolutePath_LocatesRepository confirms historical behavior.

Related: dotnet/msbuild#14093.

Migrates the shared abstract base RepositoryTask and the concrete
LocateRepository task to the MSBuild multithreaded (MT) task model.

Base RepositoryTask:
- Implements IMultiThreadableTask with a TaskEnvironment property that
  defaults to TaskEnvironment.Fallback so existing call paths and unit
  tests keep single-process (CWD) semantics with no setup.
- Absolutizes the initial path at the GetInitialPath() boundary:
  GetOrCreateRepositoryInstance now computes
  TaskEnvironment.GetAbsolutePath(initialPath) and passes the resolved
  AbsolutePath.Value to GitRepository.TryFindRepository. This removes the
  process-CWD dependency (TryFindRepository internally calls
  Path.GetFullPath, which only consults the CWD for relative inputs;
  feeding it an absolute path is MT-safe).
  Note: the implicit AbsolutePath->string conversion returns the original
  (possibly relative) input, so .Value is used explicitly.
- Sin 2: both ReportMissingRepositoryWarning call sites keep the ORIGINAL
  initialPath so warnings show the user's input, not the absolutized path.
- Edge case: GetAbsolutePath throws ArgumentException on null/empty,
  matching the previous Path.GetFullPath("") behavior.

Concrete LocateRepository:
- Annotated with [MSBuildMultiThreadableTask].

Call-chain audit: GitOperations.GetRepositoryUrl / GetSourceRoots and the
downstream GitRepository path operations combine paths with the absolute
git/working directories of the located repository, so they become MT-safe
once the initial path is absolute. No remaining
Directory.GetCurrentDirectory / Environment.CurrentDirectory /
Environment.Get/SetEnvironmentVariable / CWD-relative Path.GetFullPath in
the LocateRepository chain.

Test:
- Adds LocateRepositoryTests.RelativePath_ResolvesAgainstTaskEnvironment-
  ProjectDirectory_NotProcessCwd (Pattern A decoy CWD): creates a real
  minimal git repo, sets the process CWD to a repo-less decoy, sets the
  task's TaskEnvironment project directory to the repo, passes a relative
  Path, and asserts WorkingDirectory/RepositoryId resolve to the project
  repo. Fails if the migration is reverted.
- Extends MockEngine to IBuildEngine4 (in-memory registered task object
  store) so the cached success path can run.
- Disables assembly test parallelization to keep the CWD mutation safe.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jankratochvilcz jankratochvilcz requested a review from tmat as a code owner June 30, 2026 13:52
jankratochvilcz and others added 2 commits June 30, 2026 15:55
The implicit string conversion returns the absolute Value, not the
original input; correct the comment while keeping the explicit .Value.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Wrap GetAbsolutePath + TryFindRepository so an empty/null initial path
  reports a missing-repository warning and returns gracefully instead of
  throwing an unhandled ArgumentException (preserves pre-migration behavior;
  ExecuteImpl's catch does not handle ArgumentException).
- Rewrite the parity comment to describe the control-flow (not just exception
  type) equivalence with the old TryFindRepository path.
- Add EmptyPath_DegradesGracefully regression test (dual-fault verified: fails
  when the catch is reverted).
- Make AssemblyInfo.cs parallelization comment test-agnostic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jankratochvilcz added a commit to jankratochvilcz/sourcelink that referenced this pull request Jun 30, 2026
- Wrap GetAbsolutePath + TryFindRepository so an empty/null initial path
  reports a missing-repository warning and returns gracefully instead of
  throwing an unhandled ArgumentException (preserves pre-migration behavior;
  ExecuteImpl's catch does not handle ArgumentException). Shared base kept
  byte-identical with mt/locate-repository (dotnet#1734).
- Rewrite the parity comment to describe the control-flow equivalence.
- Add EmptyProjectDirectory_DegradesGracefully regression test.
- Make AssemblyInfo.cs parallelization comment test-agnostic (was wrongly
  naming LocateRepositoryTests).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jankratochvilcz

Copy link
Copy Markdown
Contributor Author

Review feedback addressed

Thanks for the thorough MT + expert reviews. Pushed b0b4659:

MAJOR — empty/null Path regression (graceful → crash). Confirmed and fixed. Post-migration, TaskEnvironment.GetAbsolutePath("") throws ArgumentException before TryFindRepository, and ExecuteImpl's catch only handles IOException/InvalidDataException/NotSupportedException, so it propagated as MSB4018 — whereas pre-migration the same empty input hit TryFindRepository's internal Path.GetFullPath(""), which threw the same exception but had it swallowed, yielding a graceful missing-repository warning and Execute() == true. Fix wraps the absolutize+discover step in try { … } catch (ArgumentException) { ReportMissingRepositoryWarning(initialPath); return null; } (ArgumentNullException derives from ArgumentException, so null is covered too).

Empirical note on "whitespace": I verified against Microsoft.Build 18.6.3 that GetAbsolutePath throws only on null and "" — whitespace-only (" ") does not throw (it's treated as a relative segment). So the reachable degenerate case is empty string, reachable via direct Execute() (the engine's [Required] validation doesn't run there).

Inaccurate comment. Rewritten to describe the control-flow equivalence (swallowed → warning → success), not just the matching exception type.

Regression test. Added EmptyPath_DegradesGracefully_DoesNotThrow. Dual-fault verified: it throws (fails) when the new catch is reverted, passes with the fix in place.

MINOR — assembly-wide parallelization disable. Kept (acknowledged acceptable); the CWD-mutating test makes process-global Directory.SetCurrentDirectory calls that must not race. Comment made test-agnostic.

All 437 Microsoft.Build.Tasks.Git.UnitTests (net11.0) pass; full build clean. Shared RepositoryTask.cs/AssemblyInfo.cs kept byte-identical with #1735.

var initialPath = GetInitialPath();

if (!GitRepository.TryFindRepository(initialPath, out var location))
// Resolve the initial path against the task's project directory rather than the process

@tmat tmat Jul 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Thanks, I changed it as asked

Comment thread src/Microsoft.Build.Tasks.Git.UnitTests/LocateRepositoryTests.cs Outdated
jankratochvilcz and others added 4 commits July 3, 2026 12:15
…itory)

Make the MT migration attribute-only: rather than resolving the initial
path via TaskEnvironment.GetAbsolutePath, RepositoryTask now rejects any
path that is not fully qualified. A relative/drive-relative/root-relative
path depends on the shared process CWD/drive, which is unsafe under the
multithreaded task model, so it degrades to the standard "missing
repository" warning.

- Add IsPathFullyQualified polyfill to PathUtilities (net472 lacks the BCL
  API) with a dedicated test suite (Windows/Unix/relative cases).
- Drop IMultiThreadableTask/TaskEnvironment from RepositoryTask; keep
  [MSBuildMultiThreadableTask] on the concrete LocateRepository.
- Simplify tests: revert shared MockEngine to IBuildEngine; localize a
  minimal IBuildEngine4 to LocateRepositoryTests. Cover historical
  behavior (absolute path locates repo) and CWD-independence (relative
  path rejected even when CWD is a repo).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move the inline IBuildEngine4 test engine into a shared TestUtilities.MockEngine4
next to the existing MockEngine so other tests can reuse it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jankratochvilcz jankratochvilcz marked this pull request as ready for review July 3, 2026 13:32
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