Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions GVFS/GVFS.Common/GVFSEnlistment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,13 +438,29 @@ public bool TryCreateEnlistmentSubFolders()
return true;
}

// A linked worktree keeps its own enlistment id and mount id in its own
// repo metadata, not in git config. Git resolves --local config for a
// linked worktree to the config that all worktrees share, so a worktree
// that wrote the ids there would overwrite the ids of the primary enlistment.
public string GetMountId()
{
if (this.IsWorktree)
{
RepoMetadata.TryReadIds(this.DotGVFSRoot, out _, out string mountId, out _);
return mountId;
}

return this.GetId(GVFSConstants.GitConfig.MountId);
}

public string GetEnlistmentId()
{
if (this.IsWorktree)
{
RepoMetadata.TryReadIds(this.DotGVFSRoot, out string enlistmentId, out _, out _);
return enlistmentId;
}

return this.GetId(GVFSConstants.GitConfig.EnlistmentId);
}

Expand Down
135 changes: 125 additions & 10 deletions GVFS/GVFS.Common/RepoMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,94 @@ public static bool TryInitialize(ITracer tracer, PhysicalFileSystem fileSystem,
return true;
}

/// <summary>
/// Reads the enlistment id and the mount id from the repo metadata in
/// <paramref name="dotGVFSPath"/> without initializing <see cref="Instance"/>.
/// A missing metadata file or a missing value produces an empty string.
/// </summary>
public static bool TryReadIds(string dotGVFSPath, out string enlistmentId, out string mountId, out string error)
{
enlistmentId = string.Empty;
mountId = string.Empty;

string dictionaryPath = Path.Combine(dotGVFSPath, GVFSConstants.DotGVFS.Databases.RepoMetadata);
if (!File.Exists(dictionaryPath))
{
error = null;
return true;
}

try
{
FileBasedDictionary<string, string> metadata;
if (!FileBasedDictionary<string, string>.TryCreate(NullTracer.Instance, dictionaryPath, new PhysicalFileSystem(), out metadata, out error))
{
return false;
}

using (metadata)
{
string value;
if (metadata.TryGetValue(Keys.EnlistmentId, out value))
{
enlistmentId = value;
}

if (metadata.TryGetValue(Keys.MountId, out value))
{
mountId = value;
}
}
}
catch (FileBasedCollectionException e)
{
error = e.Message;
return false;
}

return true;
}

/// <summary>
/// Creates and persists an enlistment id in the repo metadata in
/// <paramref name="dotGVFSPath"/> when it has none, without initializing
/// <see cref="Instance"/>.
/// </summary>
public static bool TryEnsureEnlistmentId(ITracer tracer, string dotGVFSPath, out string error)
{
return TryEnsureEnlistmentId(tracer, new PhysicalFileSystem(), dotGVFSPath, out error);
}

public static bool TryEnsureEnlistmentId(ITracer tracer, PhysicalFileSystem fileSystem, string dotGVFSPath, out string error)
{
string dictionaryPath = Path.Combine(dotGVFSPath, GVFSConstants.DotGVFS.Databases.RepoMetadata);
try
{
FileBasedDictionary<string, string> metadata;
if (!FileBasedDictionary<string, string>.TryCreate(tracer, dictionaryPath, fileSystem, out metadata, out error))
{
return false;
}

using (metadata)
{
string existing;
if (!metadata.TryGetValue(Keys.EnlistmentId, out existing) || string.IsNullOrWhiteSpace(existing))
{
metadata.SetValueAndFlush(Keys.EnlistmentId, CreateNewEnlistmentId(tracer));
}
}
}
catch (FileBasedCollectionException e)
{
error = e.Message;
return false;
}

error = null;
return true;
}

public static void Shutdown()
{
if (Instance != null)
Expand Down Expand Up @@ -130,16 +218,28 @@ public bool TryGetOnDiskLayoutVersion(out int majorVersion, out int minorVersion

public void SaveCloneMetadata(ITracer tracer, GVFSEnlistment enlistment)
{
this.repoMetadata.SetValuesAndFlush(
new[]
{
new KeyValuePair<string, string>(Keys.DiskLayoutMajorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion.ToString()),
new KeyValuePair<string, string>(Keys.DiskLayoutMinorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion.ToString()),
new KeyValuePair<string, string>(Keys.GitObjectsRoot, enlistment.GitObjectsRoot),
new KeyValuePair<string, string>(Keys.LocalCacheRoot, enlistment.LocalCacheRoot),
new KeyValuePair<string, string>(Keys.BlobSizesRoot, enlistment.BlobSizesRoot),
new KeyValuePair<string, string>(Keys.EnlistmentId, CreateNewEnlistmentId(tracer)),
});
this.SaveMetadata(enlistment, CreateNewEnlistmentId(tracer));
}

/// <summary>
/// Saves the metadata that a worktree mount copies from the primary
/// enlistment. Unlike <see cref="SaveCloneMetadata"/>, it keeps an
/// existing enlistment id, so the worktree id is stable across mounts.
/// </summary>
public void SaveWorktreeMetadata(ITracer tracer, GVFSEnlistment enlistment)
{
string enlistmentId;
if (!this.repoMetadata.TryGetValue(Keys.EnlistmentId, out enlistmentId) || string.IsNullOrWhiteSpace(enlistmentId))
{
enlistmentId = CreateNewEnlistmentId(tracer);
}

this.SaveMetadata(enlistment, enlistmentId);
}

public void SetMountId(string mountId)
{
this.repoMetadata.SetValueAndFlush(Keys.MountId, mountId);
}

public void SetProjectionInvalid(bool invalid)
Expand Down Expand Up @@ -267,6 +367,20 @@ private static string CreateNewEnlistmentId(ITracer tracer)
return enlistmentId;
}

private void SaveMetadata(GVFSEnlistment enlistment, string enlistmentId)
{
this.repoMetadata.SetValuesAndFlush(
new[]
{
new KeyValuePair<string, string>(Keys.DiskLayoutMajorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion.ToString()),
new KeyValuePair<string, string>(Keys.DiskLayoutMinorVersion, GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion.ToString()),
new KeyValuePair<string, string>(Keys.GitObjectsRoot, enlistment.GitObjectsRoot),
new KeyValuePair<string, string>(Keys.LocalCacheRoot, enlistment.LocalCacheRoot),
new KeyValuePair<string, string>(Keys.BlobSizesRoot, enlistment.BlobSizesRoot),
new KeyValuePair<string, string>(Keys.EnlistmentId, enlistmentId),
});
}

private void SetInvalid(string keyName, bool invalid)
{
if (invalid)
Expand Down Expand Up @@ -301,6 +415,7 @@ public static class Keys
public const string LocalCacheRoot = "LocalCacheRoot";
public const string BlobSizesRoot = "BlobSizesRoot";
public const string EnlistmentId = "EnlistmentId";
public const string MountId = "MountId";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,72 @@ public void WorktreeUsesPerWorktreePlaceholderDatabase()
}
}

[TestCase]
public void WorktreeMountKeepsPrimaryIdsAndHasStableWorktreeEnlistmentId()
{
string primaryEnlistmentId = this.GetPrimaryConfigValue("gvfs.enlistment-id");
string primaryMountId = this.GetPrimaryConfigValue("gvfs.mount-id");
Assert.IsNotEmpty(primaryEnlistmentId);
Assert.IsNotEmpty(primaryMountId);

string suffix = Guid.NewGuid().ToString("N").Substring(0, 8);
string tempDir = Path.Combine(Path.GetTempPath(), $"gvfs-ids-test-{suffix}");
string worktreePath = Path.Combine(tempDir, "wt");
string branchName = $"ids-test-branch-{suffix}";

try
{
Directory.CreateDirectory(tempDir);

ProcessResult addResult = GitHelpers.InvokeGitAgainstGVFSRepo(
this.Enlistment.RepoRoot,
$"worktree add -b {branchName} \"{worktreePath}\"");
addResult.ExitCode.ShouldEqual(0, $"worktree add failed: {addResult.Errors}");
this.AssertWorktreeMounted(worktreePath, "ids-test worktree");

GVFSEnlistment.WorktreeInfo wtInfo = GVFSEnlistment.TryGetWorktreeInfo(worktreePath);
Assert.IsNotNull(wtInfo, "Should be able to resolve worktree info");
string worktreeDotGVFS = Path.Combine(wtInfo.WorktreeGitDir, ".gvfs");

string worktreeEnlistmentId = GVFSHelpers.GetPersistedEnlistmentId(worktreeDotGVFS);
string worktreeMountId = GVFSHelpers.GetPersistedMountId(worktreeDotGVFS);
Assert.IsNotEmpty(worktreeEnlistmentId);
Assert.IsNotEmpty(worktreeMountId);
worktreeEnlistmentId.ShouldNotEqual(primaryEnlistmentId);

this.GetPrimaryConfigValue("gvfs.enlistment-id").ShouldEqual(primaryEnlistmentId, "Worktree mount changed the primary enlistment id");
this.GetPrimaryConfigValue("gvfs.mount-id").ShouldEqual(primaryMountId, "Worktree mount changed the primary mount id");

GVFSProcess worktreeGVFS = new GVFSProcess(
GVFSTestConfig.PathToGVFS,
worktreePath,
Path.Combine(worktreePath, GVFSTestConfig.DotGVFSRoot),
this.Enlistment.ServiceName);
worktreeGVFS.Unmount();
worktreeGVFS.Mount();

GVFSHelpers.GetPersistedEnlistmentId(worktreeDotGVFS).ShouldEqual(worktreeEnlistmentId, "Worktree enlistment id changed on remount");
GVFSHelpers.GetPersistedMountId(worktreeDotGVFS).ShouldNotEqual(worktreeMountId, "Worktree mount id did not change on remount");
this.GetPrimaryConfigValue("gvfs.enlistment-id").ShouldEqual(primaryEnlistmentId, "Worktree remount changed the primary enlistment id");
this.GetPrimaryConfigValue("gvfs.mount-id").ShouldEqual(primaryMountId, "Worktree remount changed the primary mount id");
}
finally
{
this.ForceCleanupWorktree(worktreePath, branchName);
if (Directory.Exists(tempDir))
{
try { Directory.Delete(tempDir, recursive: true); } catch { }
}
}
}

private string GetPrimaryConfigValue(string key)
{
ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo(this.Enlistment.RepoRoot, $"config {key}");
result.ExitCode.ShouldEqual(0, $"git config {key} failed: {result.Errors}");
return result.Output.Trim();
}

private void InitWorktreeArrays(int count, out string[] paths, out string[] branches)
{
paths = new string[count];
Expand Down
12 changes: 12 additions & 0 deletions GVFS/GVFS.FunctionalTests/Tools/GVFSHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public static class GVFSHelpers
private const string GitObjectsRootKey = "GitObjectsRoot";
private const string PlaceholdersNeedUpdate = "PlaceholdersNeedUpdate";
private const string BlobSizesRootKey = "BlobSizesRoot";
private const string EnlistmentIdKey = "EnlistmentId";
private const string MountIdKey = "MountId";

private const string PrjFSLibPath = "libPrjFSLib.dylib";
private const int PrjFSResultSuccess = 1;
Expand Down Expand Up @@ -86,6 +88,16 @@ public static string GetPersistedBlobSizesRoot(string dotGVFSRoot)
return GetPersistedValue(dotGVFSRoot, BlobSizesRootKey);
}

public static string GetPersistedEnlistmentId(string dotGVFSRoot)
{
return GetPersistedValue(dotGVFSRoot, EnlistmentIdKey);
}

public static string GetPersistedMountId(string dotGVFSRoot)
{
return GetPersistedValue(dotGVFSRoot, MountIdKey);
}

public static void SQLiteBlobSizesDatabaseHasEntry(string blobSizesDbPath, string blobSha, long blobSize)
{
RunSqliteCommand(blobSizesDbPath, command =>
Expand Down
21 changes: 18 additions & 3 deletions GVFS/GVFS.Mount/InProcessMount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -585,16 +585,16 @@ private void InitializeWorktreeMetadata()
// can persist them into the worktree's metadata
this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot);

// Initialize the worktree's own metadata with cache paths,
// disk layout version, and a new enlistment ID
// Initialize the worktree's own metadata with cache paths and the
// disk layout version. The worktree keeps its existing enlistment ID.
if (!RepoMetadata.TryInitialize(this.tracer, dotGVFSRoot, out error))
{
this.FailMountAndExit("Failed to initialize worktree metadata: " + error);
}

try
{
RepoMetadata.Instance.SaveCloneMetadata(this.tracer, this.enlistment);
RepoMetadata.Instance.SaveWorktreeMetadata(this.tracer, this.enlistment);
}
finally
{
Expand Down Expand Up @@ -1945,6 +1945,21 @@ private void LogEnlistmentInfoAndSetConfigValues()
metadata.Add("PhysicalDiskInfo", GVFSPlatform.Instance.GetPhysicalDiskInfo(this.enlistment.WorkingDirectoryRoot, sizeStatsOnly: false));
this.tracer.RelatedEvent(EventLevel.Informational, "EnlistmentInfo", metadata, Keywords.Telemetry);

if (this.enlistment.IsWorktree)
{
// See GVFSEnlistment.GetMountId for why a worktree does not write its ids to git config.
try
{
RepoMetadata.Instance.SetMountId(mountId);
}
catch (FileBasedCollectionException e)
{
this.tracer.RelatedWarning("Could not update repo metadata with mount id, error: " + e.Message);
}

return;
}

GitProcess git = new GitProcess(this.enlistment);
GitProcess.Result configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.EnlistmentId, RepoMetadata.Instance.EnlistmentId, replaceAll: true);
if (configResult.ExitCodeIsFailure)
Expand Down
8 changes: 8 additions & 0 deletions GVFS/GVFS.Mount/InProcessMountVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ private void UnhandledGVFSExceptionHandler(ITracer tracer, object sender, Unhand

private JsonTracer CreateTracer(GVFSEnlistment enlistment, EventLevel verbosity, Keywords keywords)
{
if (enlistment.IsWorktree)
{
// Create the worktree's enlistment ID before the tracer reads it, so that
// the first mount of a new worktree has an enlistment ID in its telemetry.
// If this fails, the tracer uses an empty ID and the mount continues.
RepoMetadata.TryEnsureEnlistmentId(NullTracer.Instance, enlistment.DotGVFSRoot, out _);
}

JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "GVFSMount", enlistment.GetEnlistmentId(), enlistment.GetMountId());
tracer.AddLogFileEventListener(
GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.MountProcess),
Expand Down
Loading
Loading