|
| 1 | +# FolderDiffIL4DotNet (English) |
| 2 | + |
| 3 | +This repository hosts a .NET console application that compares two folders, classifies the differences, and writes a detailed Markdown report. When both inputs are .NET assemblies, the app ignores build-specific artifacts such as the `// MVID:` line, so assemblies that behave the same are treated as equal even if they were produced at different times. |
| 4 | + |
| 5 | +> Looking for the Japanese version? See [README.md](README.md). |
| 6 | +
|
| 7 | +## Requirements |
| 8 | + |
| 9 | +- .NET SDK 8.x |
| 10 | +- macOS / Windows / Linux / Unix-like OS (e.g., FreeBSD) |
| 11 | +- IL disassembler (the app automatically probes candidates in this order) |
| 12 | + - Preferred: `dotnet-ildasm` or `dotnet ildasm` |
| 13 | + - Fallback: `ilspycmd` |
| 14 | + |
| 15 | +Installation example: |
| 16 | + |
| 17 | +```bash |
| 18 | +dotnet tool install --global dotnet-ildasm |
| 19 | +# add $HOME/.dotnet/tools (Unix) or %USERPROFILE%\.dotnet\tools (Windows) to PATH if necessary |
| 20 | +``` |
| 21 | + |
| 22 | +```bash |
| 23 | +dotnet tool install -g ilspycmd |
| 24 | +# add $HOME/.dotnet/tools (Unix) or %USERPROFILE%\.dotnet\tools (Windows) to PATH if necessary |
| 25 | +``` |
| 26 | + |
| 27 | +## CI (GitHub Actions) |
| 28 | + |
| 29 | +The repository includes `.github/workflows/dotnet.yml`. The workflow runs for pushes and pull requests targeting `main`, and it can also be triggered manually via `workflow_dispatch`. |
| 30 | + |
| 31 | +- `actions/checkout` uses `fetch-depth: 0` so Nerdbank.GitVersioning can traverse the full commit history. |
| 32 | +- `actions/setup-dotnet` honors `global.json`, installing the same SDK (e.g., 8.0.413) that you use locally before running `dotnet restore` and a Release build. |
| 33 | +- `dotnet test` runs only when a project file matching `**/*Tests.csproj` or `**/*.Tests.csproj` exists, so repositories without tests still succeed. |
| 34 | +- NuGet packages inside `~/.nuget/packages` are cached via `actions/cache` to accelerate subsequent builds. |
| 35 | +- Release artifacts are produced with `dotnet publish FolderDiffIL4DotNet.csproj --output publish`, stripped of debug symbols (`*.pdb`), and uploaded as `FolderDiffIL4DotNet` via `actions/upload-artifact`. |
| 36 | + |
| 37 | +How to use it: |
| 38 | + |
| 39 | +1. Push this repo to GitHub—no additional configuration required. |
| 40 | +2. If your default branch is not `main`, edit `on.push.branches` and `on.pull_request.branches` in the workflow. |
| 41 | +3. When you add test projects, ensure their file names contain `Tests`, or adjust the job condition accordingly. |
| 42 | +4. Download the Release build from "Artifacts > FolderDiffIL4DotNet" on the workflow run page. |
| 43 | + |
| 44 | +## What the app does |
| 45 | + |
| 46 | +- Recursively compares the old folder (CLI arg #1) and the new folder (CLI arg #2). |
| 47 | +- Tracks each file as `MD5Match`, `MD5Mismatch`, `ILMatch`, `ILMismatch`, `TextMatch`, or `TextMismatch`. |
| 48 | +- Groups files into `Unchanged`, `Added`, `Removed`, and `Modified` buckets. |
| 49 | +- Writes per-bucket listings to `Reports/<report label>/diff_report.md`; paths are relative for `Unchanged`/`Modified` and absolute for `Added`/`Removed`. |
| 50 | +- Summarizes counts per bucket in the same report. |
| 51 | +- Optionally writes ignored files, unchanged files, timestamps, and warnings when at least one `MD5Mismatch` exists. |
| 52 | + |
| 53 | +## File comparison flow |
| 54 | + |
| 55 | +1. **MD5 hash** – if hashes match, the file is `Unchanged (MD5Match)`. |
| 56 | +2. **IL diff** – if the file is a .NET assembly (detected via PE/CLR headers, regardless of extension), the app disassembles both versions, strips `// MVID:` lines, and compares them line by line. Matches become `Unchanged (ILMatch)`; mismatches become `Modified (ILMismatch)`. |
| 57 | +3. **Text diff** – if the extension appears in `TextFileExtensions`, a line-based text diff runs. Matches are `Unchanged (TextMatch)`; mismatches are `Modified (TextMismatch)`. |
| 58 | +4. **Fallback** – remaining files are treated as `Modified (MD5Mismatch)`. |
| 59 | + |
| 60 | +## Configuration (`config.json`) |
| 61 | + |
| 62 | +Place `config.json` next to the executable. Example: |
| 63 | + |
| 64 | +```json |
| 65 | +{ |
| 66 | + "IgnoredExtensions": [".cache", ".DS_Store", ".db", ".ilcache", ".log", ".pdb"], |
| 67 | + "TextFileExtensions": [ |
| 68 | + ".asax", |
| 69 | + ".ascx", |
| 70 | + ".asmx", |
| 71 | + ".aspx", |
| 72 | + ".bat", |
| 73 | + ".c", |
| 74 | + ".cmd", |
| 75 | + ".config", |
| 76 | + ".cpp", |
| 77 | + ".cs", |
| 78 | + ".cshtml", |
| 79 | + ".csproj", |
| 80 | + ".csx", |
| 81 | + ".css", |
| 82 | + ".csv", |
| 83 | + ".editorconfig", |
| 84 | + ".env", |
| 85 | + ".fs", |
| 86 | + ".fsi", |
| 87 | + ".fsproj", |
| 88 | + ".fsx", |
| 89 | + ".gitattributes", |
| 90 | + ".gitignore", |
| 91 | + ".gitmodules", |
| 92 | + ".go", |
| 93 | + ".gql", |
| 94 | + ".graphql", |
| 95 | + ".h", |
| 96 | + ".hpp", |
| 97 | + ".htm", |
| 98 | + ".html", |
| 99 | + ".http", |
| 100 | + ".ini", |
| 101 | + ".js", |
| 102 | + ".json", |
| 103 | + ".jsx", |
| 104 | + ".less", |
| 105 | + ".manifest", |
| 106 | + ".md", |
| 107 | + ".mod", |
| 108 | + ".nlog", |
| 109 | + ".nuspec", |
| 110 | + ".plist", |
| 111 | + ".props", |
| 112 | + ".ps1", |
| 113 | + ".psd1", |
| 114 | + ".psm1", |
| 115 | + ".py", |
| 116 | + ".razor", |
| 117 | + ".resx", |
| 118 | + ".rst", |
| 119 | + ".sass", |
| 120 | + ".scss", |
| 121 | + ".sh", |
| 122 | + ".sln", |
| 123 | + ".sql", |
| 124 | + ".sqlproj", |
| 125 | + ".sum", |
| 126 | + ".svg", |
| 127 | + ".targets", |
| 128 | + ".toml", |
| 129 | + ".ts", |
| 130 | + ".tsv", |
| 131 | + ".tsx", |
| 132 | + ".txt", |
| 133 | + ".vb", |
| 134 | + ".vbproj", |
| 135 | + ".vue", |
| 136 | + ".xaml", |
| 137 | + ".xml", |
| 138 | + ".yaml", |
| 139 | + ".yml" |
| 140 | + ], |
| 141 | + "MaxLogGenerations": 5, |
| 142 | + "ShouldIncludeUnchangedFiles": true, |
| 143 | + "ShouldIncludeIgnoredFiles": true, |
| 144 | + "ShouldOutputILText": true, |
| 145 | + "ShouldOutputFileTimestamps": true, |
| 146 | + "MaxParallelism": 0, |
| 147 | + "EnableILCache": true, |
| 148 | + "ILCacheDirectoryAbsolutePath": "", |
| 149 | + "ILCacheStatsLogIntervalSeconds": 60, |
| 150 | + "ILCacheMaxDiskFileCount": 0, |
| 151 | + "ILCacheMaxDiskMegabytes": 0, |
| 152 | + "OptimizeForNetworkShares": false, |
| 153 | + "AutoDetectNetworkShares": true |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +| Key | Description | |
| 158 | +| --- | --- | |
| 159 | +| `IgnoredExtensions` | Excludes matching extensions from comparison (e.g., `.pdb`). | |
| 160 | +| `TextFileExtensions` | Treats matching extensions as text, diffed line by line. Include the dot (e.g., `.cs`, `.json`). | |
| 161 | +| `MaxLogGenerations` | Number of log files kept in rotation. | |
| 162 | +| `ShouldIncludeUnchangedFiles` | Whether to list `Unchanged` files inside `Reports/<label>/diff_report.md`. | |
| 163 | +| `ShouldIncludeIgnoredFiles` | Whether to output ignored files in the `## [ x ] Ignored Files` section (before `Unchanged`). | |
| 164 | +| `ShouldOutputILText` | Writes IL dumps to `Reports/<label>/IL/old` and `.../IL/new`. | |
| 165 | +| `ShouldOutputFileTimestamps` | Adds last modified timestamps to each file line inside the report. | |
| 166 | +| `MaxParallelism` | Degree of parallelism for file comparisons. `0` or omitted uses the logical core count. | |
| 167 | +| `EnableILCache` | Caches IL disassembly results (MD5 + tool/version) in memory and optionally on disk. | |
| 168 | +| `ILCacheDirectoryAbsolutePath` | Custom cache folder. Blank defaults to `<exe>/ILCache` with LRU + TTL control. | |
| 169 | +| `ILCacheStatsLogIntervalSeconds` | Interval (seconds) for logging IL cache statistics. `<= 0` falls back to 60 seconds. | |
| 170 | +| `ILCacheMaxDiskFileCount` | Upper bound for disk cache files. `<= 0` disables trimming. Oldest entries are removed first. | |
| 171 | +| `ILCacheMaxDiskMegabytes` | Disk cache size limit (MB). `<= 0` disables trimming. Oldest entries are removed until under the limit. | |
| 172 | +| `OptimizeForNetworkShares` | Optimizes comparisons on NAS/SMB shares by skipping MD5 pre-warming, reducing parallelism, and forcing sequential diffing of large text files. | |
| 173 | +| `AutoDetectNetworkShares` | Detects network paths automatically (UNC on Windows, `statfs` on macOS, `/proc/mounts`/`/etc/mtab` on Linux/Unix) and enables the same optimizations automatically. | |
| 174 | + |
| 175 | +Notes: |
| 176 | + |
| 177 | +- Files without extensions are still compared. Add an empty string to `TextFileExtensions` if you want them treated as text. |
| 178 | +- .NET "extensionless" executables (apphosts) may keep the same MD5 even after rebuilding. |
| 179 | + |
| 180 | +## Usage |
| 181 | + |
| 182 | +1. Review and adjust `config.json` next to the executable. |
| 183 | +2. Run the app with the following arguments: |
| 184 | + 1. Absolute path to the old (baseline) folder. |
| 185 | + 2. Absolute path to the new folder. |
| 186 | + 3. Report label (used as the subfolder name under `Reports`). |
| 187 | + 4. Optional `--no-pause` to skip the "Press any key" prompt. |
| 188 | +3. The prompt is automatically skipped when the process is non-interactive (I/O redirection). |
| 189 | + |
| 190 | +Build and run example: |
| 191 | + |
| 192 | +```bash |
| 193 | +dotnet build |
| 194 | +dotnet run "/Users/UserA/workspace/old" "/Users/UserA/workspace/new" "YYYYMMDD" --no-pause |
| 195 | +``` |
| 196 | + |
| 197 | +The console shows progress, and after completion the report is available at `Reports/<label>/diff_report.md`. |
| 198 | + |
| 199 | +After writing the report, the following files are marked read-only (failures only generate warnings): |
| 200 | + |
| 201 | +- `diff_report.md` |
| 202 | +- `IL/old/*_IL.txt` (when `ShouldOutputILText` is true) |
| 203 | +- `IL/new/*_IL.txt` (when `ShouldOutputILText` is true) |
| 204 | + |
| 205 | +## Generated artifacts |
| 206 | + |
| 207 | +- `Logs/log_YYYYMMDD.log` – application logs. Entries older than `MaxLogGenerations` are deleted. |
| 208 | +- If `ShouldOutputILText` is true: |
| 209 | + - `Reports/<label>/IL/old/*.txt` – IL dumps (build-specific noise removed) for files from the old folder. |
| 210 | + - `Reports/<label>/IL/new/*.txt` – IL dumps for the new folder. |
| 211 | + - IL dumps exclude lines that start with `// MVID:`. |
| 212 | + |
| 213 | +## Performance optimizations |
| 214 | + |
| 215 | +| Feature | Summary | Notes | |
| 216 | +| --- | --- | --- | |
| 217 | +| Parallel diffing | Compares files in parallel up to `MaxParallelism`. | Balances CPU and I/O usage. | |
| 218 | +| IL cache | Reuses IL text based on MD5 + tool label (command + version). | In-memory cache (LRU up to 2000 items, TTL 12h) with optional disk persistence. | |
| 219 | +| MD5 pre-warming | Precomputes MD5 for all targets in parallel before diffing. | Evens out cache key generation time. | |
| 220 | +| IL cache prefetch | Promotes disk cache entries into memory before diffing. | Further reduces disassembler launches. | |
| 221 | +| Parallel text diff | Files ≥512 KiB are split into 64 KiB chunks compared in parallel. | Only determines equality (no diff output). | |
| 222 | +| Tool failure blacklist | Skips IL tools that failed repeatedly (default: 3 times) for 10 minutes. | Reduces repeated launch overhead. | |
| 223 | + |
| 224 | +### IL cache notes |
| 225 | + |
| 226 | +- File names in the disk cache are sanitized (invalid chars/colon replaced with `_`, overly long names shortened) to avoid NTFS alternate data stream quirks. |
| 227 | +- Disk cache trimming obeys both LRU and the `ILCacheMaxDiskFileCount` / `ILCacheMaxDiskMegabytes` settings. |
| 228 | + |
| 229 | +## Versioning (Nerdbank.GitVersioning) |
| 230 | + |
| 231 | +The project uses [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) to produce SemVer versions. |
| 232 | + |
| 233 | +- `version.json` declares release channels such as `main` or tags like `v1.2.3`. |
| 234 | +- The generated `AssemblyInformationalVersion` is recorded in `Reports/<label>/diff_report.md`. |
| 235 | +- You can override the version manually via `dotnet build /p:Version=1.2.3` when needed. |
| 236 | + |
| 237 | +Tagging example: |
| 238 | + |
| 239 | +```bash |
| 240 | +git tag v1.0.0 |
| 241 | +git push origin v1.0.0 |
| 242 | +``` |
0 commit comments