Skip to content

Auto-dehydrate before checkout when hydration thresholds exceeded#1931

Open
tyrielv wants to merge 2 commits intomicrosoft:masterfrom
tyrielv:tyrielv/checkout-dehydrate
Open

Auto-dehydrate before checkout when hydration thresholds exceeded#1931
tyrielv wants to merge 2 commits intomicrosoft:masterfrom
tyrielv:tyrielv/checkout-dehydrate

Conversation

@tyrielv
Copy link
Copy Markdown
Contributor

@tyrielv tyrielv commented Mar 27, 2026

Summary

Adds opt-in automatic dehydration before branch-switching checkouts. When configured hydration thresholds are exceeded, the pre-command hook runs gvfs dehydrate --confirm before the checkout proceeds, keeping the repo clean for subsequent operations.

Config (all disabled by default)

git config gvfs.auto-dehydrate-placeholder-percent 40
git config gvfs.auto-dehydrate-modified-percent 5
git config gvfs.auto-dehydrate-folder-percent 50

Any threshold set to a non-negative value enables the feature. OR logic — dehydrate triggers if any threshold is exceeded. Set to -1 (or unset) to disable.

Review Guide

Commit 1 — Infrastructure (safe, no behavior change)

  • GitCommandLineParser.TryGetBranchSwitchTarget — checkout/switch arg parsing
  • GitIndexHelper.ReadEntryCount — extracted from EnlistmentHydrationSummary
  • LibGit2Repo.GetConfigInt / GetConfigIntOrDefault — git_config_get_int32 P/Invoke
  • EnlistmentHydrationSummary — percentage helper properties
  • Unit tests for parser and index helper

Commit 2 — Feature (gated behind config flags)

  • Program.TryDehydrateBeforeCheckout — main hook logic, wrapped in try/catch
  • TryGetCachedHydrationStatus — named pipe query to mount (5s timeout)
  • GetOrCreateSharedRepoInvoker — lazy shared LibGit2RepoInvoker
  • GitStatusCache.UpdateHydrationSummary — also runs when thresholds configured
  • Functional test

Key safety properties

  • Fully inert when unconfigured — all thresholds default to -1, TryDehydrateBeforeCheckout returns immediately
  • Best-effort only — entire method wrapped in catch (Exception); never blocks checkout
  • No new dependencies on hot path — LibGit2RepoInvoker created lazily, only when thresholds are set

tyrielv added 2 commits March 27, 2026 16:15
- GitCommandLineParser.TryGetBranchSwitchTarget: detect branch-switching
  checkout/switch commands, handling -b/-B/-c/-C, --detach, --orphan,
  --patch, and file checkout patterns
- GitIndexHelper.ReadEntryCount: reusable 4-byte index header read,
  extracted from EnlistmentHydrationSummary
- LibGit2Repo.GetConfigInt: git_config_get_int32 P/Invoke for reading
  integer config values on live (non-snapshot) config handles
- LibGit2RepoInvoker.GetConfigIntOrDefault: convenience wrapper
- EnlistmentHydrationSummary: add percentage properties
  (PlaceholderFilePercent, ModifiedFilePercent, etc.)
- GVFSConstants: add auto-dehydrate threshold config constants
  (gvfs.auto-dehydrate-{placeholder,modified,folder}-percent)
- Unit tests for TryGetBranchSwitchTarget and GitIndexHelper

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
Pre-command hook detects branch-switching checkouts and triggers
automatic dehydration when configured hydration thresholds are exceeded.

Flow:
1. Parse checkout/switch args — bail if not a branch switch
2. Read threshold configs via libgit2 — bail if all disabled (-1)
3. Query mount's cached hydration summary via named pipe
4. Compare placeholder/modified/folder percentages (OR logic)
5. Check cached git status (5s timeout) — pass --no-status if clean
6. Run gvfs dehydrate --confirm

Config settings (all default to -1 = disabled):
  gvfs.auto-dehydrate-placeholder-percent
  gvfs.auto-dehydrate-modified-percent
  gvfs.auto-dehydrate-folder-percent

GitStatusCache.UpdateHydrationSummary now also runs when any
auto-dehydrate threshold is configured, so users don't need to
separately enable gvfs.show-hydration-status.

Lazy<LibGit2RepoInvoker> shared across pre-command hook avoids
multiple expensive repo handle initializations.

Functional test verifies end-to-end dehydrate trigger.

Assisted-by: Claude Opus 4.6
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
@tyrielv tyrielv marked this pull request as ready for review March 27, 2026 23:54
@@ -125,10 +142,9 @@ private static bool HasShortFlag(string arg, string flag)

private static bool ConfigurationAllowsHydrationStatus()
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LazyRepoInvoker may need to be initialized, it might be null if called from the PostCommandHook path.

() => new LibGit2RepoInvoker(NullTracer.Instance, normalizedCurrentDirectory));
try
{
TryDehydrateBeforeCheckout(args);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Check if TryDehydrateBeforeCheckout runs before acquiring gvfs lock, may run into concurrency issues if it's not locking first.

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.

The locking here (plus status gating) is actually really complicated and I need to step back and think about it. We don't want to try to dehydrate if the status isn't clean, but checking if the status is clean if the cache isn't up-to-date can take a really long time if there are lots of hydrated files - the time when we most want to dehydrate.

{
shouldDehydrate = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can you check if git status --porcelain actually gates dehydration


if (!shouldDehydrate)
{
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider checking for exit code gvfs dehydrate --confirm, if there's a failure, checkout might still proceed.

}

/// <summary>
/// Query the mount process for the cached hydration status via named pipe.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

50x timeout here 100ms -> 5000ms and 50ms -> 2000ms is a big change, what's the rational here?

return null;
}

/// <summary>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We might end up with recursive hooks if git status --porcelain gets spawned in the pre-command hook. Might create a risk for lock contention?

GVFSConstants.GitConfig.DehydrateOnCheckoutDisabled);
int modifiedThreshold = repoInvoker.GetConfigIntOrDefault(
GVFSConstants.GitConfig.DehydrateOnCheckoutModifiedPercent,
GVFSConstants.GitConfig.DehydrateOnCheckoutDisabled);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we handle control + c interrupts? Question: Why are we specifying do not interrupt?

@tyrielv tyrielv force-pushed the tyrielv/checkout-dehydrate branch from f340599 to 7bcfed4 Compare March 31, 2026 17:49
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