├── .gitignore ├── LICENSE ├── Readme.md ├── examples └── example.md ├── file1.txt ├── file2.txt ├── index.ts ├── package-lock.json ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | diff.js 3 | fileDiff.ts 4 | file.patch -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Shivam Yadav 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # TSDiffTool: TypeScript-Powered File Diff Tool 2 | 3 | TSDiffTool is an elegant file diffing and patching tool built in TypeScript, highlighting edit distance and patch generation concepts. 4 | 5 | ## Key Features 6 | 7 | - **Text File Differences:** Determines and calculates the minimal changes required to transform one text file into another. 8 | - **Patch Generation:** Creates concise patch files representing the identified differences. 9 | - **Patch Application:** Modifies files based on a provided patch. 10 | 11 | ## Algorithm Insights 12 | 13 | The core algorithm, utilizing dynamic programming for edit distance (Levenshtein distance), boasts the following complexities: 14 | 15 | - **Time Complexity:** O(N * M) (N and M are lengths of input files). 16 | - **Space Complexity:** O(min(N , M)) for storing the distance matrix. 17 | 18 | The time complexity of the edit distance algorithm is O(N * M) in the worst case. However, Hirschberg's algorithm might have a smaller constant factor, leading to potentially better performance in practice. 19 | 20 | 21 | ## Usage 22 | 23 | 1. **Installation:** 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | 2. **Generate a Diff:** 29 | ```bash 30 | ts-node index.ts diff > 31 | ``` 32 | - `` and ``: The paths to the text files you want to compare. 33 | 34 | 3. **Apply a Patch:** 35 | ```bash 36 | ts-node index.ts patch 37 | ``` 38 | - ``: The path to the text file you want to patch. 39 | - ``: The patch file generated from the diff operation. 40 | 41 | ## Examples 42 | Explore the - [`examples`](examples/example.md) directory for practical usage scenarios. 43 | 44 | ## Code Overview 45 | 46 | ### Edit Distance Algorithm 47 | 48 | ```typescript 49 | // Sample code snippet for editDistance function 50 | function editDistance(s1: T[], s2: T[]): [Action, number, T][] { 51 | // Implementation details... 52 | return patch; 53 | } 54 | ``` 55 | 56 | ### Subcommands 57 | 58 | ```typescript 59 | // Sample code snippet for DiffSubcommand 60 | class DiffSubcommand extends Subcommand { 61 | // Implementation details... 62 | } 63 | 64 | // Sample code snippet for PatchSubcommand 65 | class PatchSubcommand extends Subcommand { 66 | // Implementation details... 67 | } 68 | 69 | // Sample code snippet for HelpSubcommand 70 | class HelpSubcommand extends Subcommand { 71 | // Implementation details... 72 | } 73 | ``` 74 | 75 | ## Contributing 76 | 77 | Contributions, pull requests, and suggestions are encouraged to enhance TSDiffTool. 78 | 79 | ## License 80 | 81 | TSDiffTool is released under the MIT License. 82 | -------------------------------------------------------------------------------- /examples/example.md: -------------------------------------------------------------------------------- 1 | | Use Case | Scenario | Usage | 2 | | --- | --- | --- | 3 | | **Version Control System Integration** | You are building a version control system (VCS) or integrating a diff tool into an existing VCS system (e.g., Git). | The TypeScript Diff Tool can be used to calculate the difference between different versions of a file. This information is crucial for a VCS to understand what lines were added, modified, or deleted between commits. | 4 | | **Code Review Tool** | In a code review tool, developers review each other's code changes before merging them into the main branch. | The TypeScript Diff Tool can be used to generate a patch file representing the changes made by a developer. The reviewer can then apply this patch to their local copy to see the exact modifications, facilitating a more detailed code review. | 5 | | **Text Document Comparison** | You are building an application for comparing versions of text documents, helping users identify changes between different revisions. | The TypeScript Diff Tool can be employed to compare two versions of a document, highlighting insertions, deletions, and modifications. This can be useful for collaborative writing platforms or document versioning systems. | 6 | | **Automated Patching in Deployment** | During the deployment process, you need to update configuration files or scripts on the server while preserving any manual changes. | The TypeScript Diff Tool can generate a patch file representing the changes. During deployment, this patch can be applied to update the files on the server, ensuring that manual modifications are retained. | 7 | | **Localization File Updates** | When managing localization files for different languages, you want to update translations without losing existing translations. | The TypeScript Diff Tool can be used to generate a patch file when updating a base translation file. This patch can then be applied to the translated files, updating only the necessary parts without affecting custom translations. | -------------------------------------------------------------------------------- /file1.txt: -------------------------------------------------------------------------------- 1 | This is the first sample file. 2 | It contains some lines of text. 3 | There are a few sentences here. 4 | And a final line. 5 | -------------------------------------------------------------------------------- /file2.txt: -------------------------------------------------------------------------------- 1 | This is the second sample file. 2 | It has some similar lines of text. 3 | A few sentences have been changed. 4 | And an extra line has been added. 5 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | type Action = "I" | "A" | "R"; 4 | 5 | function readEntireFile(filePath: string): string { 6 | const content = fs.readFileSync(filePath, "utf8"); 7 | return content; 8 | } 9 | 10 | function editDistance(s1: T[], s2: T[]): [Action, number, T][] { 11 | const m1 = s1.length; 12 | const m2 = s2.length; 13 | 14 | function calculateDistances(strip: number[]): number[] { 15 | const distances: number[] = [strip[0]]; 16 | 17 | for (let i = 1; i < m2 + 1; i++) { 18 | distances[i] = Math.min( 19 | distances[i - 1] + 1, 20 | strip[i], 21 | strip[i - 1] + (s1[m1 - strip[i - 1]] === s2[i - 1] ? 0 : 1) 22 | ); 23 | } 24 | return distances; 25 | } 26 | 27 | const patch: [Action, number, T][] = []; 28 | let strip1: number[] = new Array(m2 + 1).fill(0); 29 | let strip2: number[] = new Array(m2 + 1).fill(0); 30 | for (let i = 1; i < m1 + 1; i++) { 31 | strip2[0] = i; 32 | 33 | for (let j = 1; j < m2 + 1; j++) { 34 | strip2[j] = 35 | s1[i - 1] === s2[j - 1] 36 | ? strip1[j - 1] 37 | : Math.min(strip1[j - 1], strip1[j], strip2[j - 1]) + 1; 38 | } 39 | 40 | if (i < m1) { 41 | strip1 = calculateDistances(strip2); 42 | } 43 | } 44 | 45 | let n1 = m1; 46 | let n2 = m2; 47 | 48 | while (n1 > 0 || n2 > 0) { 49 | if (n1 === 0) { 50 | n2 -= 1; 51 | patch.push(["A", n2, s2[n2]]); 52 | } else if (n2 === 0) { 53 | n1 -= 1; 54 | patch.push(["R", n1, s1[n1]]); 55 | } else if (s1[n1 - 1] === s2[n2 - 1]) { 56 | n1 -= 1; 57 | n2 -= 1; 58 | } else if (strip2[n2] === strip1[n1 - 1] + 1) { 59 | n1 -= 1; 60 | patch.push(["R", n1, s1[n1]]); 61 | } else { 62 | n2 -= 1; 63 | patch.push(["A", n2, s2[n2]]); 64 | } 65 | } 66 | patch.sort((a, b) => a[1] - b[1]); 67 | 68 | patch.reverse(); 69 | 70 | return patch; 71 | } 72 | 73 | const PATCH_LINE_REGEXP: RegExp = /([AR]) (\d+) (.*)/; 74 | 75 | class Subcommand { 76 | name: string; 77 | signature: string; 78 | description: string; 79 | 80 | constructor(name: string, signature: string, description: string) { 81 | this.name = name; 82 | this.signature = signature; 83 | this.description = description; 84 | } 85 | 86 | run(program: string, args: string[]): number { 87 | throw new Error("Method not implemented."); 88 | } 89 | } 90 | 91 | class DiffSubcommand extends Subcommand { 92 | constructor() { 93 | super( 94 | "diff", 95 | " ", 96 | "print the difference between the files" 97 | ); 98 | } 99 | 100 | run(program: string, args: string[]): number { 101 | if (args.length < 2) { 102 | console.log(`Usage: ${program} ${this.name} ${this.signature}`); 103 | console.log(`ERROR: not enough files were provided to ${this.name}`); 104 | return 1; 105 | } 106 | 107 | let [file_path1, file_path2, ...rest] = args; 108 | 109 | try { 110 | const lines1 = readEntireFile(file_path1).split("\n"); 111 | const lines2 = readEntireFile(file_path2).split("\n"); 112 | 113 | const patch = editDistance(lines1, lines2); 114 | 115 | for (const [action, n, line] of patch) { 116 | console.log(`${action} ${n} ${line}`); 117 | } 118 | 119 | return 0; 120 | } catch (error) { 121 | console.error(`Error during execution: ${error}`); 122 | return 1; 123 | } 124 | } 125 | } 126 | 127 | class PatchSubcommand extends Subcommand { 128 | constructor() { 129 | super( 130 | "patch", 131 | " ", 132 | "patch the file with the given patch" 133 | ); 134 | } 135 | 136 | run(program: string, args: string[]): number { 137 | console.log(`Executing ${this.name} subcommand...`); 138 | if (args.length < 2) { 139 | console.log(`Usage: ${program} ${this.name} ${this.signature}`); 140 | console.log( 141 | `ERROR: not enough arguments were provided to ${this.name} a file` 142 | ); 143 | return 1; 144 | } 145 | 146 | let [file_path, patch_path, ...rest] = args; 147 | const lines = readEntireFile(file_path).split("\n"); 148 | const patch: [Action, number, string][] = []; 149 | let ok = true; 150 | 151 | for (const [row, line] of readEntireFile(patch_path) 152 | .split("\n") 153 | .entries()) { 154 | if (line.length === 0) { 155 | continue; 156 | } 157 | const m = line.match(PATCH_LINE_REGEXP); 158 | if (m === null) { 159 | console.log(`${patch_path}:${row + 1}: Invalid patch action: ${line}`); 160 | ok = false; 161 | continue; 162 | } 163 | patch.push([m[1] as Action, parseInt(m[2]), m[3]]); 164 | } 165 | if (!ok) { 166 | return 1; 167 | } 168 | 169 | for (const [action, row, line] of patch.reverse()) { 170 | if (action === "A") { 171 | lines.splice(row, 0, line); 172 | } else if (action === "R") { 173 | lines.splice(row, 1); 174 | } else { 175 | throw new Error("unreachable"); 176 | } 177 | } 178 | 179 | fs.writeFileSync(file_path, lines.join("\n")); 180 | return 0; 181 | } 182 | } 183 | 184 | class HelpSubcommand extends Subcommand { 185 | constructor() { 186 | super("help", "[subcommand]", "print this help message"); 187 | } 188 | 189 | run(program: string, args: string[]): number { 190 | console.log(`Executing ${this.name} subcommand...`); 191 | if (args.length === 0) { 192 | usage(program); 193 | return 0; 194 | } 195 | 196 | const [subcmdName, ...rest] = args; 197 | 198 | const subcmd = findSubcommand(subcmdName); 199 | if (subcmd !== undefined) { 200 | console.log(`Usage: ${program} ${subcmd.name} ${subcmd.signature}`); 201 | console.log(` ${subcmd.description}`); 202 | return 0; 203 | } 204 | 205 | usage(program); 206 | console.log(`ERROR: unknown subcommand ${subcmdName}`); 207 | suggestClosestSubcommandIfExists(subcmdName); 208 | return 1; 209 | } 210 | } 211 | 212 | const SUBCOMMANDS: Subcommand[] = [ 213 | new DiffSubcommand(), 214 | new PatchSubcommand(), 215 | new HelpSubcommand(), 216 | ]; 217 | 218 | function usage(program: string): void { 219 | console.log(`Generating usage for ${program}...`); 220 | const width = Math.max( 221 | ...SUBCOMMANDS.map((subcmd) => `${subcmd.name} ${subcmd.signature}`.length) 222 | ); 223 | console.log(`Usage: ${program} [OPTIONS]`); 224 | console.log("Subcommands:"); 225 | for (const subcmd of SUBCOMMANDS) { 226 | const command = `${subcmd.name} ${subcmd.signature}`.padEnd(width); 227 | console.log(` ${command} ${subcmd.description}`); 228 | } 229 | } 230 | 231 | function suggestClosestSubcommandIfExists(subcmdName: string): void { 232 | console.log(`Suggesting closest subcommand for: ${subcmdName}`); 233 | const candidates = SUBCOMMANDS.filter((subcmd) => { 234 | return ( 235 | editDistance(Array.from(subcmdName), Array.from(subcmd.name)).length < 3 236 | ); 237 | }).map((subcmd) => subcmd.name); 238 | 239 | if (candidates.length > 0) { 240 | console.log("Maybe you meant:"); 241 | for (const name of candidates) { 242 | console.log(` ${name}`); 243 | } 244 | } 245 | } 246 | 247 | function findSubcommand(subcmdName: string): Subcommand | undefined { 248 | return SUBCOMMANDS.find((subcmd) => subcmd.name === subcmdName); 249 | } 250 | 251 | function main() { 252 | const [program, ...args] = process.argv; 253 | 254 | if (args.length === 0) { 255 | usage(program); 256 | console.log("ERROR: no subcommand is provided"); 257 | return 1; 258 | } 259 | 260 | const [fiel, subcmdName, ...rest] = args; 261 | 262 | const subcmd = findSubcommand(subcmdName); 263 | if (subcmd) { 264 | return subcmd.run(program, rest); 265 | } 266 | 267 | usage(program); 268 | console.log(`ERROR: unknown subcommand ${subcmdName}`); 269 | suggestClosestSubcommandIfExists(subcmdName); 270 | return 1; 271 | } 272 | 273 | if (require.main === module) { 274 | process.exit(main()); 275 | } 276 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsdifftool", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "tsdifftool", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/node": "^20.10.5", 13 | "typescript": "^5.3.3" 14 | } 15 | }, 16 | "node_modules/@types/node": { 17 | "version": "20.10.5", 18 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", 19 | "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", 20 | "dev": true, 21 | "dependencies": { 22 | "undici-types": "~5.26.4" 23 | } 24 | }, 25 | "node_modules/typescript": { 26 | "version": "5.3.3", 27 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 28 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 29 | "dev": true, 30 | "bin": { 31 | "tsc": "bin/tsc", 32 | "tsserver": "bin/tsserver" 33 | }, 34 | "engines": { 35 | "node": ">=14.17" 36 | } 37 | }, 38 | "node_modules/undici-types": { 39 | "version": "5.26.5", 40 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 41 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 42 | "dev": true 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsdifftool", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/node": "^20.10.5", 13 | "typescript": "^5.3.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "strict": true, 7 | "esModuleInterop": true 8 | } 9 | } 10 | --------------------------------------------------------------------------------