@@ -173,13 +173,25 @@ private void HandleAllDirectoryOperations()
173173 case DiffTreeResult . Operations . Add :
174174 try
175175 {
176- Directory . CreateDirectory ( absoluteTargetPath ) ;
176+ if ( treeOp . SourcePath != null )
177+ {
178+ this . ApplyCaseOnlyDirectoryRename ( treeOp , absoluteTargetPath ) ;
179+ }
180+ else
181+ {
182+ Directory . CreateDirectory ( absoluteTargetPath ) ;
183+ }
177184 }
178185 catch ( Exception ex )
179186 {
180187 EventMetadata metadata = new EventMetadata ( ) ;
181- metadata . Add ( "Operation" , "CreateDirectory" ) ;
188+ metadata . Add ( "Operation" , treeOp . SourcePath != null ? "RenameDirectory" : "CreateDirectory" ) ;
182189 metadata . Add ( nameof ( treeOp . TargetPath ) , absoluteTargetPath ) ;
190+ if ( treeOp . SourcePath != null )
191+ {
192+ metadata . Add ( nameof ( treeOp . SourcePath ) , treeOp . SourcePath ) ;
193+ }
194+
183195 this . tracer . RelatedError ( metadata , ex . Message ) ;
184196 this . HasFailures = true ;
185197 }
@@ -222,6 +234,62 @@ private void HandleAllDirectoryOperations()
222234 }
223235 }
224236
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+
225293 private void HandleAllFileDeleteOperations ( )
226294 {
227295 string path ;
0 commit comments