Skip to content
Merged
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
6 changes: 3 additions & 3 deletions GVFS/FastFetch/CheckoutPrefetcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ public override void Prefetch(string branchOrCommit, bool isBranch)
bool allowIndexMetadataUpdateFromWorkingTree = this.allowIndexMetadataUpdateFromWorkingTree || checkout.UpdatedWholeTree;
newIndex.UpdateFileSizesAndTimes(checkout.AddedOrEditedLocalFiles, allowIndexMetadataUpdateFromWorkingTree, shouldSignIndex, sourceIndex);

// All the slow stuff is over, so we will now move the final index into .git\index, shortly followed by
// All the slow stuff is over, so we will now move the final index into place, shortly followed by
// updating the ref files and releasing index.lock.
string indexPath = Path.Combine(this.Enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName);
string indexPath = this.Enlistment.GitIndexPath;
this.Tracer.RelatedEvent(EventLevel.Informational, "MoveUpdatedIndexToFinalLocation", new EventMetadata() { { "UpdatedIndex", indexGen.TemporaryIndexFilePath }, { "Index", indexPath } });
File.Delete(indexPath);
File.Move(indexGen.TemporaryIndexFilePath, indexPath);
Expand Down Expand Up @@ -196,7 +196,7 @@ protected override void UpdateRefs(string branchOrCommit, bool isBranch, GitRefs

private Index GetSourceIndex()
{
string indexPath = Path.Combine(this.Enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName);
string indexPath = this.Enlistment.GitIndexPath;

if (File.Exists(indexPath))
{
Expand Down
98 changes: 98 additions & 0 deletions GVFS/GVFS.Common/GVFSEnlistment.Shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ private static WorktreeInfo TryParseWorktreeGitFile(string worktreeRoot, string
/// Returns the working directory paths of all worktrees registered
/// under <paramref name="gitDir"/>/worktrees by reading each entry's
/// gitdir file. The primary worktree is not included.
/// Unreadable registrations are skipped. Do not use this method to guard destructive operations.
/// </summary>
public static string[] GetKnownWorktreePaths(string gitDir)
{
Expand Down Expand Up @@ -212,6 +213,103 @@ public static string[] GetKnownWorktreePaths(string gitDir)
return paths.ToArray();
}

/// <summary>
/// Reads registrations for destructive-operation guards.
/// Missing or empty reverse pointers are ignored only when no other metadata remains.
/// </summary>
public static bool TryGetKnownWorktreePaths(
string gitDir,
out string[] paths,
out string error,
out Exception exception)
{
ArgumentNullException.ThrowIfNull(gitDir);
paths = null;
error = null;
exception = null;
string pathToRead = gitDir;
try
{
// Unlike Directory.Exists, this reports access failures instead of treating them as absence.
if ((File.GetAttributes(gitDir) & FileAttributes.Directory) == 0)
{
error = $"Invalid Git directory at '{gitDir}': expected a directory.";
return false;
}
string worktreesDir = Path.Combine(gitDir, "worktrees");
pathToRead = worktreesDir;
try
{
File.GetAttributes(worktreesDir);
}
catch (FileNotFoundException)
{
File.GetAttributes(gitDir);
paths = Array.Empty<string>();
return true;
}
catch (DirectoryNotFoundException)
{
File.GetAttributes(gitDir);
paths = Array.Empty<string>();
return true;
}

List<string> worktreePaths = new List<string>();
foreach (string entry in Directory.GetDirectories(worktreesDir))
{
pathToRead = Path.Combine(entry, "gitdir");
string gitdirContent = string.Empty;
try
{
using (FileStream stream = File.OpenRead(pathToRead))
{
using (StreamReader reader = new StreamReader(stream))
{
gitdirContent = reader.ReadToEnd();
}
}
}
catch (FileNotFoundException)
{
}

if (gitdirContent.Length == 0)
{
foreach (string metadataPath in Directory.GetFileSystemEntries(entry))
{
if (!string.Equals(Path.GetFileName(metadataPath), "gitdir", StringComparison.Ordinal))
{
error = $"Cannot verify worktree registration at '{pathToRead}': the pointer is missing or empty, but metadata remains at '{metadataPath}'.";
return false;
}
}

continue;
}

gitdirContent = gitdirContent.Trim().Replace('/', Path.DirectorySeparatorChar);
if (string.IsNullOrEmpty(gitdirContent) ||
!string.Equals(Path.GetFileName(gitdirContent), GVFSConstants.DotGit.Root, StringComparison.Ordinal))
{
error = $"Invalid worktree registration at '{pathToRead}': expected a path to a .git file.";
return false;
}

worktreePaths.Add(Path.GetDirectoryName(Path.GetFullPath(gitdirContent, entry)));
}

paths = worktreePaths.ToArray();
return true;
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException || ex is ArgumentException)
{
error = $"Failed to read worktree registrations at '{pathToRead}': {ex.Message}";
exception = ex;
return false;
}
}

public class WorktreeInfo
{
public const string EnlistmentRootFileName = "gvfs-enlistment-root";
Expand Down
18 changes: 12 additions & 6 deletions GVFS/GVFS.Common/Git/GitIndexGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class GitIndexGenerator
0, 0, 0, 0 // file length
};

private readonly string indexPath;
private readonly string indexLockPath;

private Enlistment enlistment;
Expand All @@ -63,7 +64,8 @@ public GitIndexGenerator(ITracer tracer, Enlistment enlistment, bool shouldHashI
// other is from the 'gvfs repair' verb. That environment is special in that it only runs on unmounted
// repo's, so 'index.lock' is irrelevant as a locking mechanism in that context. There can't be git
// commands to lock out.
this.indexLockPath = Path.Combine(enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName + ".lock2");
this.indexPath = enlistment.GitIndexPath;
this.indexLockPath = this.indexPath + ".lock2";
Comment thread
tyrielv marked this conversation as resolved.
}

public string TemporaryIndexFilePath => this.indexLockPath;
Expand All @@ -82,7 +84,12 @@ public GitIndexGenerator(ITracer tracer, Enlistment enlistment, bool shouldHashI
/// </remarks>
public void CreateFromRef(string refName, uint indexVersion, bool isFinal)
{
using (ITracer updateIndexActivity = this.tracer.StartActivity("CreateFromHeadTree", EventLevel.Informational))
EventMetadata metadata = new EventMetadata
{
{ "IndexPath", this.indexPath },
{ "IsWorktree", this.enlistment is GVFSEnlistment gvfsEnlistment && gvfsEnlistment.IsWorktree },
};
using (ITracer updateIndexActivity = this.tracer.StartActivity("CreateFromHeadTree", EventLevel.Informational, metadata))
{
Thread entryWritingThread = new Thread(() => this.WriteAllEntries(indexVersion, isFinal));
entryWritingThread.Start();
Expand Down Expand Up @@ -245,11 +252,10 @@ private byte[] GetIndexHash()
return new byte[20];
}

private void ReplaceExistingIndex()
internal void ReplaceExistingIndex()
{
string indexPath = Path.Combine(this.enlistment.DotGitRoot, GVFSConstants.DotGit.IndexName);
File.Delete(indexPath);
File.Move(this.indexLockPath, indexPath);
File.Delete(this.indexPath);
File.Move(this.indexLockPath, this.indexPath);
}

private class LsTreeEntry
Expand Down
Loading
Loading