Skip to content

Commit 4748e73

Browse files
authored
Merge pull request #2 from derrickstolee/dstolee/case-rename-fixes
Address review of case-rename support: ordering, rollback, observability, performance, tests
2 parents ea81e68 + cbd7c82 commit 4748e73

8 files changed

Lines changed: 348 additions & 149 deletions

File tree

GVFS/FastFetch/CheckoutStage.cs

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -175,22 +175,7 @@ private void HandleAllDirectoryOperations()
175175
{
176176
if (treeOp.SourcePath != null)
177177
{
178-
// Case-only rename: rename the directory from old casing to new casing
179-
string absoluteSourcePath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, treeOp.SourcePath);
180-
if (Directory.Exists(absoluteSourcePath))
181-
{
182-
// Directory.Move throws IOException for case-only renames,
183-
// so use a two-step rename through a temporary name.
184-
string tempPath = absoluteTargetPath.TrimEnd(Path.DirectorySeparatorChar) + "_caseRename_" + Guid.NewGuid().ToString("N");
185-
Directory.Move(absoluteSourcePath.TrimEnd(Path.DirectorySeparatorChar), tempPath);
186-
Directory.Move(tempPath, absoluteTargetPath.TrimEnd(Path.DirectorySeparatorChar));
187-
}
188-
else
189-
{
190-
// Parent directory may have already been renamed, fixing this child's path.
191-
// Just ensure the directory exists.
192-
Directory.CreateDirectory(absoluteTargetPath);
193-
}
178+
this.ApplyCaseOnlyDirectoryRename(treeOp, absoluteTargetPath);
194179
}
195180
else
196181
{
@@ -249,6 +234,62 @@ private void HandleAllDirectoryOperations()
249234
}
250235
}
251236

237+
/// <summary>
238+
/// Apply a case-only directory rename produced by DiffHelper, where
239+
/// <paramref name="treeOp"/>.SourcePath carries the old casing and
240+
/// <paramref name="absoluteTargetPath"/> is the new (post-rename) absolute path.
241+
///
242+
/// Directory.Move throws IOException for case-only renames on Windows, so the
243+
/// rename is performed in two steps through a temporary name. If the second
244+
/// move fails the directory is moved back to the original casing so a retry
245+
/// sees a consistent working tree.
246+
///
247+
/// If the source directory is missing it usually means an outer parent rename
248+
/// has already moved the children into place (Windows preserves child casing
249+
/// through a parent rename when the children's tree SHAs were unchanged); the
250+
/// fallback creates the target directory so the operation is idempotent.
251+
/// Exceptions propagate to the caller's existing error handler.
252+
/// </summary>
253+
private void ApplyCaseOnlyDirectoryRename(DiffTreeResult treeOp, string absoluteTargetPath)
254+
{
255+
string absoluteSourcePath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, treeOp.SourcePath);
256+
if (!Directory.Exists(absoluteSourcePath))
257+
{
258+
Directory.CreateDirectory(absoluteTargetPath);
259+
return;
260+
}
261+
262+
string trimmedSourcePath = absoluteSourcePath.TrimEnd(Path.DirectorySeparatorChar);
263+
string trimmedTargetPath = absoluteTargetPath.TrimEnd(Path.DirectorySeparatorChar);
264+
string tempPath = trimmedTargetPath + "_caseRename_" + Guid.NewGuid().ToString("N");
265+
266+
Directory.Move(trimmedSourcePath, tempPath);
267+
try
268+
{
269+
Directory.Move(tempPath, trimmedTargetPath);
270+
}
271+
catch
272+
{
273+
// The first move succeeded but the second failed. Try to restore the
274+
// original casing so a retry starts from a consistent state; if
275+
// restoration also fails, the outer catch will log the original
276+
// exception and the temp directory will be left behind for manual
277+
// recovery.
278+
if (Directory.Exists(tempPath) && !Directory.Exists(trimmedSourcePath))
279+
{
280+
try
281+
{
282+
Directory.Move(tempPath, trimmedSourcePath);
283+
}
284+
catch
285+
{
286+
}
287+
}
288+
289+
throw;
290+
}
291+
}
292+
252293
private void HandleAllFileDeleteOperations()
253294
{
254295
string path;

GVFS/GVFS.Common/Git/DiffTreeResult.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@ public enum Operations
3939
public ushort TargetMode { get; set; }
4040

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

4753
public static DiffTreeResult ParseFromDiffTreeLine(string line)
4854
{

0 commit comments

Comments
 (0)