Skip to content
Open
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
72 changes: 70 additions & 2 deletions GVFS/FastFetch/CheckoutStage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,25 @@ private void HandleAllDirectoryOperations()
case DiffTreeResult.Operations.Add:
try
{
Directory.CreateDirectory(absoluteTargetPath);
if (treeOp.SourcePath != null)
{
this.ApplyCaseOnlyDirectoryRename(treeOp, absoluteTargetPath);
}
else
{
Directory.CreateDirectory(absoluteTargetPath);
}
}
catch (Exception ex)
{
EventMetadata metadata = new EventMetadata();
metadata.Add("Operation", "CreateDirectory");
metadata.Add("Operation", treeOp.SourcePath != null ? "RenameDirectory" : "CreateDirectory");
metadata.Add(nameof(treeOp.TargetPath), absoluteTargetPath);
if (treeOp.SourcePath != null)
{
metadata.Add(nameof(treeOp.SourcePath), treeOp.SourcePath);
}

this.tracer.RelatedError(metadata, ex.Message);
this.HasFailures = true;
}
Expand Down Expand Up @@ -222,6 +234,62 @@ private void HandleAllDirectoryOperations()
}
}

/// <summary>
/// Apply a case-only directory rename produced by DiffHelper, where
/// <paramref name="treeOp"/>.SourcePath carries the old casing and
/// <paramref name="absoluteTargetPath"/> is the new (post-rename) absolute path.
///
/// Directory.Move throws IOException for case-only renames on Windows, so the
/// rename is performed in two steps through a temporary name. If the second
/// move fails the directory is moved back to the original casing so a retry
/// sees a consistent working tree.
///
/// If the source directory is missing it usually means an outer parent rename
/// has already moved the children into place (Windows preserves child casing
/// through a parent rename when the children's tree SHAs were unchanged); the
/// fallback creates the target directory so the operation is idempotent.
/// Exceptions propagate to the caller's existing error handler.
/// </summary>
private void ApplyCaseOnlyDirectoryRename(DiffTreeResult treeOp, string absoluteTargetPath)
{
string absoluteSourcePath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, treeOp.SourcePath);
if (!Directory.Exists(absoluteSourcePath))
{
Directory.CreateDirectory(absoluteTargetPath);
return;
}

string trimmedSourcePath = absoluteSourcePath.TrimEnd(Path.DirectorySeparatorChar);
string trimmedTargetPath = absoluteTargetPath.TrimEnd(Path.DirectorySeparatorChar);
string tempPath = trimmedTargetPath + "_caseRename_" + Guid.NewGuid().ToString("N");

Directory.Move(trimmedSourcePath, tempPath);
try
{
Directory.Move(tempPath, trimmedTargetPath);
}
catch
{
// The first move succeeded but the second failed. Try to restore the
// original casing so a retry starts from a consistent state; if
// restoration also fails, the outer catch will log the original
// exception and the temp directory will be left behind for manual
// recovery.
if (Directory.Exists(tempPath) && !Directory.Exists(trimmedSourcePath))
{
try
{
Directory.Move(tempPath, trimmedSourcePath);
}
catch
{
}
}

throw;
}
}

private void HandleAllFileDeleteOperations()
{
string path;
Expand Down
12 changes: 12 additions & 0 deletions GVFS/GVFS.Common/Git/DiffTreeResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ public enum Operations
public ushort SourceMode { get; set; }
public ushort TargetMode { get; set; }

/// <summary>
/// Old-cased path of a case-only directory rename, set by DiffHelper when
/// collapsing a Delete+Add pair under the case-insensitive comparer. When
/// non-null the operation represents a rename from SourcePath to TargetPath
/// and consumers (currently CheckoutStage) must rename the directory on
/// disk instead of treating the operation as a plain Add. Always null for
/// file operations, Modify, Delete, and non-rename Add entries. The setter
/// is intentionally restricted to the assembly so only the parser can
/// produce this annotation.
/// </summary>
public string SourcePath { get; internal set; }

public static DiffTreeResult ParseFromDiffTreeLine(string line)
{
if (string.IsNullOrEmpty(line))
Expand Down
Loading
Loading