@@ -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 ;
0 commit comments