git_tools: add rename detection and proper handling of moved files

Enhance GitRawDiff to properly handle file renames and moves by:

1. Add -M flag to git diff commands to enable rename detection
2. Update parseRawDiff to handle the different output format for renames:
   - Rename format: :oldmode newmode oldhash newhash R100 old_path new_path
   - Split rename operations into separate delete and add entries
   - This allows Monaco diff view to display both old and new files

3. Update DiffFile comment to document rename/copy status codes

The fix addresses GitHub issue #120 where Monaco diff view would error
when displaying files that were both modified and renamed. By splitting
renames into delete/add pairs, the existing UI can handle moved files
without requiring frontend changes.

Fixes #120

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s172724445cadbd68k
diff --git a/webui/src/web-components/sketch-diff-file-picker.ts b/webui/src/web-components/sketch-diff-file-picker.ts
index eb94a5f..cadafae 100644
--- a/webui/src/web-components/sketch-diff-file-picker.ts
+++ b/webui/src/web-components/sketch-diff-file-picker.ts
@@ -205,7 +205,8 @@
   formatFileOption(file: GitDiffFile): string {
     const statusSymbol = this.getFileStatusSymbol(file.status);
     const changesInfo = this.getChangesInfo(file);
-    return `${statusSymbol} ${file.path}${changesInfo}`;
+    const pathInfo = this.getPathInfo(file);
+    return `${statusSymbol} ${pathInfo}${changesInfo}`;
   }
 
   /**
@@ -231,6 +232,18 @@
   }
 
   /**
+   * Get path information for display, handling renames and copies
+   */
+  getPathInfo(file: GitDiffFile): string {
+    if (file.old_path && file.old_path !== "") {
+      // For renames and copies, show old_path -> new_path
+      return `${file.old_path} → ${file.path}`;
+    }
+    // For regular files, just show the path
+    return file.path;
+  }
+
+  /**
    * Get a short symbol for the file status
    */
   getFileStatusSymbol(status: string): string {
@@ -244,6 +257,14 @@
       case "R":
         return "R";
       default:
+        // Handle copy statuses like C096, C100, etc.
+        if (status.toUpperCase().startsWith("C")) {
+          return "C";
+        }
+        // Handle rename statuses like R096, R100, etc.
+        if (status.toUpperCase().startsWith("R")) {
+          return "R";
+        }
         return "?";
     }
   }
@@ -262,6 +283,14 @@
       case "R":
         return "Renamed";
       default:
+        // Handle copy statuses like C096, C100, etc.
+        if (status.toUpperCase().startsWith("C")) {
+          return "Copied";
+        }
+        // Handle rename statuses like R096, R100, etc.
+        if (status.toUpperCase().startsWith("R")) {
+          return "Renamed";
+        }
         return "Unknown";
     }
   }