├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── package.json ├── src └── index.js └── test └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["stage-0", "es2015"], 3 | "plugins": [ 4 | "add-module-exports" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb-base" 4 | ], 5 | "rules": { 6 | "consistent-return": 0, 7 | "eqeqeq": 0, 8 | "func-names": 0, 9 | "guard-for-in": 0, 10 | "global-require": 0, 11 | "one-var": [2, "never"], 12 | "padded-blocks": 0, 13 | "vars-on-top": 0, 14 | "max-len": 0, 15 | "no-console": 0, 16 | "no-param-reassign": 0, 17 | "no-use-before-define": 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 6 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/fgnass/diffparser.svg?branch=master)](https://travis-ci.org/fgnass/diffparser) 2 | 3 | # Unified diff parser for Node and the browser 4 | 5 | This project is a ES2015 version of 6 | https://github.com/sergeyt/parse-diff. 7 | 8 | It comes with a full test suite and in addition to line numbers also provides `position` information as required by the [GitHub Comments API](https://developer.github.com/v3/pulls/comments/#create-a-comment). 9 | 10 | ```js 11 | import parse from 'diffparser'; 12 | 13 | const diff = ` 14 | diff --git a/file b/file 15 | index 123..456 789 16 | --- a/file 17 | +++ b/file 18 | @@ -1,2 +1,2 @@ 19 | - line1 20 | + line2 21 | `; 22 | 23 | parse(diff); 24 | ``` 25 | 26 | This will return an array (one entry per file) with the following structure: 27 | 28 | ```json 29 | [ 30 | { 31 | "from": "file", 32 | "to": "file", 33 | "chunks": [ 34 | { 35 | "content": "@@ -1,2 +1,2 @@", 36 | "changes": [ 37 | { 38 | "type": "del", 39 | "del": true, 40 | "oldLine": 1, 41 | "position": 1, 42 | "content": "- line1" 43 | }, 44 | { 45 | "type": "add", 46 | "add": true, 47 | "newLine": 1, 48 | "position": 2, 49 | "content": "+ line2" 50 | } 51 | ], 52 | "oldStart": 1, 53 | "oldLines": 2, 54 | "newStart": 1, 55 | "newLines": 2 56 | } 57 | ], 58 | "deletions": 1, 59 | "additions": 1, 60 | "index": [ 61 | "123..456", 62 | "789" 63 | ] 64 | } 65 | ] 66 | ``` 67 | 68 | ## Optional Rename Detection 69 | 70 | You can pass `{ findRenames: true }` as option in order to detect renamed files: 71 | 72 | ```js 73 | const diff = ` 74 | diff --git a/bar b/bar 75 | new file mode 100644 76 | index 0000000..4e4b354 77 | --- /dev/null 78 | +++ b/bar 79 | @@ -0,0 +1,2 @@ 80 | +this is a 81 | +sample file 82 | diff --git a/foo b/foo 83 | deleted file mode 100644 84 | index 4e4b354..0000000 85 | --- a/foo 86 | +++ /dev/null 87 | @@ -1,2 +0,0 @@ 88 | -this is a 89 | -sample file 90 | `; 91 | 92 | parse(diff, { findRenames: true }); 93 | ``` 94 | 95 | This will return the following array: 96 | 97 | ```js 98 | [ 99 | { 100 | "renamed": true, 101 | "from": "foo", 102 | "to": "bar" 103 | } 104 | ] 105 | ``` 106 | 107 | # License 108 | 109 | MIT 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diffparser", 3 | "version": "2.0.1", 4 | "description": "Unified diff parser", 5 | "author": "Felix Gnass ", 6 | "repository": "fgnass/diffparser", 7 | "license": "MIT", 8 | "main": "lib", 9 | "scripts": { 10 | "test": "mocha --require babel-core/register test", 11 | "build": "babel src --out-dir lib", 12 | "prepublish": "npm run build" 13 | }, 14 | "devDependencies": { 15 | "babel-cli": "^6.9.0", 16 | "babel-core": "^6.26.3", 17 | "babel-plugin-add-module-exports": "^0.2.1", 18 | "babel-preset-es2015": "^6.9.0", 19 | "babel-preset-stage-0": "^6.5.0", 20 | "eslint": "^2.11.1", 21 | "eslint-config-airbnb-base": "^3.0.1", 22 | "eslint-plugin-import": "^1.8.1", 23 | "mocha": "^5.2.0", 24 | "unexpected": "^10.13.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse unified diff input 3 | * see: http://www.gnu.org/software/diffutils/manual/diffutils.html#Unified-Format 4 | */ 5 | export default function (input, opts = {}) { 6 | if (!input) return []; 7 | if (input.match(/^\s+$/)) return []; 8 | 9 | const lines = input.split('\n'); 10 | if (lines.length == 0) return []; 11 | 12 | const files = []; 13 | let file = null; 14 | let oldLine = 0; 15 | let newLine = 0; 16 | let position = 0; 17 | let current = null; 18 | 19 | function start(line) { 20 | const [from, to] = parseFile(line); 21 | file = { 22 | from, 23 | to, 24 | chunks: [], 25 | deletions: 0, 26 | additions: 0, 27 | }; 28 | files.push(file); 29 | position = 0; 30 | } 31 | 32 | function restart() { 33 | if (!file || file.chunks.length) start(); 34 | } 35 | 36 | function newFile() { 37 | restart(); 38 | file.new = true; 39 | file.from = '/dev/null'; 40 | } 41 | 42 | function deletedFile() { 43 | restart(); 44 | file.deleted = true; 45 | file.to = '/dev/null'; 46 | } 47 | 48 | function index(line) { 49 | restart(); 50 | file.index = line.split(' ').slice(1); 51 | } 52 | 53 | function fromFile(line) { 54 | restart(); 55 | file.from = parseFileFallback(line); 56 | } 57 | 58 | function toFile(line) { 59 | restart(); 60 | file.to = parseFileFallback(line); 61 | } 62 | 63 | function chunk(line, match) { 64 | const [, oldStart, oldLines, newStart, newLines] = match.map(l => +(l || 0)); 65 | oldLine = oldStart; 66 | newLine = newStart; 67 | current = { 68 | content: line, 69 | changes: [], 70 | oldStart, 71 | oldLines, 72 | newStart, 73 | newLines, 74 | }; 75 | file.chunks.push(current); 76 | if (!position) position = 1; 77 | } 78 | 79 | function del(line) { 80 | current.changes.push({ 81 | type: 'del', 82 | del: true, 83 | oldLine: oldLine++, 84 | position: position++, 85 | content: line, 86 | }); 87 | file.deletions++; 88 | } 89 | 90 | function add(line) { 91 | current.changes.push({ 92 | type: 'add', 93 | add: true, 94 | newLine: newLine++, 95 | position: position++, 96 | content: line, 97 | }); 98 | file.additions++; 99 | } 100 | 101 | const noeol = '\\ No newline at end of file'; 102 | 103 | function normal(line) { 104 | if (!file) return; 105 | current.changes.push({ 106 | type: 'normal', 107 | normal: true, 108 | oldLine: line !== noeol ? oldLine++ : undefined, 109 | newLine: line !== noeol ? newLine++ : undefined, 110 | position: position++, 111 | content: line, 112 | }); 113 | } 114 | 115 | const schema = [ 116 | [/^\s+/, normal], 117 | [/^diff\s/, start], 118 | [/^new file mode \d+$/, newFile], 119 | [/^deleted file mode \d+$/, deletedFile], 120 | [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index], 121 | [/^---\s/, fromFile], 122 | [/^\+\+\+\s/, toFile], 123 | [/^@@\s+\-(\d+),?(\d*)\s+\+(\d+),?(\d*)\s@@/, chunk], 124 | [/^-/, del], 125 | [/^\+/, add], 126 | ]; 127 | 128 | function parse(line) { 129 | return schema.some(p => { 130 | const [pattern, handler] = p; 131 | if (typeof handler !== 'function') { 132 | throw new Error(`${pattern} has no handler`); 133 | } 134 | const m = line.match(pattern); 135 | if (m) { 136 | handler(line, m); 137 | return true; 138 | } 139 | return false; 140 | }); 141 | } 142 | 143 | lines.forEach(parse); 144 | if (opts.findRenames) consolidateRenames(files); 145 | return files; 146 | } 147 | 148 | function getContent(file) { 149 | return file.chunks.map(chunk => chunk.changes.map(c => c.content.slice(1))).join('\n'); 150 | } 151 | 152 | function consolidateRenames(files) { 153 | const newFiles = files.filter(f => f.new); 154 | newFiles.forEach(newFile => { 155 | const newContent = getContent(newFile); 156 | const i = files.findIndex(f => f.deleted && getContent(f) == newContent); 157 | if (~i) { 158 | const oldFile = files[i]; 159 | files.splice(i, 1); 160 | delete newFile.new; 161 | delete newFile.chunks; 162 | delete newFile.deletions; 163 | delete newFile.additions; 164 | delete newFile.index; 165 | newFile.renamed = true; 166 | newFile.from = oldFile.from; 167 | } 168 | }); 169 | } 170 | 171 | function parseFile(s) { 172 | if (!s) return []; 173 | const fileNames = s.split(' ').slice(-2); 174 | return fileNames.map(f => f.replace(/^(a|b)\//, '')); 175 | } 176 | 177 | function parseFileFallback(s) { 178 | s = s.replace(/^\s*(\++|-+)/, '').trim(); 179 | 180 | // ignore possible timestamp 181 | const t = (/\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/).exec(s); 182 | if (t) s = s.substring(0, t.index).trim(); 183 | 184 | // ignore git prefixes a/ or b/ 185 | return s.match(/^(a|b)\//) ? s.substr(2) : s; 186 | } 187 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | import expect from 'unexpected'; 3 | import parse from '../src'; 4 | 5 | describe('diff parser', () => { 6 | it('should parse null', () => { 7 | expect(parse(null), 'to be empty'); 8 | }); 9 | 10 | it('should parse empty string', () => { 11 | expect(parse(''), 'to be empty'); 12 | }); 13 | 14 | it('should parse whitespace', () => { 15 | expect(parse(' '), 'to be empty'); 16 | }); 17 | 18 | it('should parse simple git-like diff', () => { 19 | const diff = ` 20 | diff --git a/file b/file 21 | index 123..456 789 22 | --- a/file 23 | +++ b/file 24 | @@ -1,2 +1,2 @@ 25 | - line1 26 | + line2 27 | `; 28 | const files = parse(diff); 29 | expect(files, 'to have length', 1); 30 | const file = files[0]; 31 | expect(file, 'to have properties', { 32 | from: 'file', 33 | to: 'file', 34 | }); 35 | expect(file.chunks, 'to have length', 1); 36 | const chunk = file.chunks[0]; 37 | expect(chunk.content, 'to be', '@@ -1,2 +1,2 @@'); 38 | expect(chunk.changes, 'to have length', 2); 39 | expect(chunk.changes[0].type, 'to be', 'del'); 40 | expect(chunk.changes[0].content, 'to be', '- line1'); 41 | expect(chunk.changes[0].position, 'to be', 1); 42 | expect(chunk.changes[0].oldLine, 'to be', 1); 43 | expect(chunk.changes[1].type, 'to be', 'add'); 44 | expect(chunk.changes[1].content, 'to be', '+ line2'); 45 | expect(chunk.changes[1].position, 'to be', 2); 46 | expect(chunk.changes[1].newLine, 'to be', 1); 47 | }); 48 | 49 | it('should parse diff with new file mode line', () => { 50 | const diff = ` 51 | diff --git a/test b/test 52 | new file mode 100644 53 | index 0000000..db81be4 54 | --- /dev/null 55 | +++ b/test 56 | @@ -0,0 +1,2 @@ 57 | +line1 58 | +line2 59 | `; 60 | const files = parse(diff); 61 | expect(files, 'to have length', 1); 62 | const file = files[0]; 63 | expect(file.new, 'to be true'); 64 | expect(file.from, 'to be', '/dev/null'); 65 | expect(file.to, 'to be', 'test'); 66 | 67 | const chunk = file.chunks[0]; 68 | expect(chunk.content, 'to be', '@@ -0,0 +1,2 @@'); 69 | expect(chunk.changes, 'to satisfy', [ 70 | { content: '+line1', position: 1, type: 'add', newLine: 1 }, 71 | { content: '+line2', position: 2, type: 'add', newLine: 2 }, 72 | ]); 73 | }); 74 | 75 | it('should parse diff with deleted file mode line', () => { 76 | const diff = ` 77 | diff --git a/test b/test 78 | deleted file mode 100644 79 | index db81be4..0000000 80 | --- b/test 81 | +++ /dev/null 82 | @@ -1,2 +0,0 @@ 83 | -line1 84 | -line2 85 | `; 86 | const files = parse(diff); 87 | expect(files, 'to have length', 1); 88 | const file = files[0]; 89 | expect(file.deleted, 'to be true'); 90 | expect(file.from, 'to be', 'test'); 91 | expect(file.to, 'to be', '/dev/null'); 92 | const chunk = file.chunks[0]; 93 | expect(chunk.content, 'to be', '@@ -1,2 +0,0 @@'); 94 | expect(chunk.changes, 'to satisfy', [ 95 | { content: '-line1', position: 1, type: 'del', oldLine: 1 }, 96 | { content: '-line2', position: 2, type: 'del', oldLine: 2 }, 97 | ]); 98 | }); 99 | 100 | it('should parse diff with single line files', () => { 101 | const diff = ` 102 | diff --git a/file1 b/file1 103 | deleted file mode 100644 104 | index db81be4..0000000 105 | --- b/file1 106 | +++ /dev/null 107 | @@ -1 +0,0 @@ 108 | -line1 109 | diff --git a/file2 b/file2 110 | new file mode 100644 111 | index 0000000..db81be4 112 | --- /dev/null 113 | +++ b/file2 114 | @@ -0,0 +1 @@ 115 | +line1 116 | `; 117 | const files = parse(diff); 118 | expect(files, 'to have length', 2); 119 | 120 | const [f1, f2] = files; 121 | expect(f1.deleted, 'to be true'); 122 | expect(f1.from, 'to be', 'file1'); 123 | expect(f1.to, 'to be', '/dev/null'); 124 | 125 | expect(f2.new, 'to be true'); 126 | expect(f2.from, 'to be', '/dev/null'); 127 | expect(f2.to, 'to be', 'file2'); 128 | 129 | const c1 = f1.chunks[0]; 130 | expect(c1.content, 'to be', '@@ -1 +0,0 @@'); 131 | expect(c1.changes, 'to satisfy', [ 132 | { content: '-line1', position: 1, type: 'del', oldLine: 1 }, 133 | ]); 134 | const c2 = f2.chunks[0]; 135 | expect(c2.content, 'to be', '@@ -0,0 +1 @@'); 136 | expect(c2.changes, 'to satisfy', [ 137 | { content: '+line1', position: 1, type: 'add', newLine: 1 }, 138 | ]); 139 | }); 140 | 141 | it('should parse multiple files in diff', () => { 142 | const diff = ` 143 | diff --git a/file1 b/file1 144 | index 123..456 789 145 | --- a/file1 146 | +++ b/file1 147 | @@ -1,2 +1,2 @@ 148 | - line1 149 | + line2 150 | diff --git a/file2 b/file2 151 | index 123..456 789 152 | --- a/file2 153 | +++ b/file2 154 | @@ -1,3 +1,3 @@ 155 | - line1 156 | + line2 157 | `; 158 | const files = parse(diff); 159 | expect(files, 'to have length', 2); 160 | 161 | const [f1, f2] = files; 162 | expect(f1, 'to satisfy', { from: 'file1', to: 'file1' }); 163 | expect(f2, 'to satisfy', { from: 'file2', to: 'file2' }); 164 | 165 | const c1 = f1.chunks[0]; 166 | expect(c1.content, 'to be', '@@ -1,2 +1,2 @@'); 167 | expect(c1.changes, 'to satisfy', [ 168 | { content: '- line1', position: 1, type: 'del', oldLine: 1 }, 169 | { content: '+ line2', position: 2, type: 'add', newLine: 1 }, 170 | ]); 171 | 172 | const c2 = f2.chunks[0]; 173 | expect(c2.content, 'to be', '@@ -1,3 +1,3 @@'); 174 | expect(c2.changes, 'to satisfy', [ 175 | { content: '- line1', type: 'del', oldLine: 1 }, 176 | { content: '+ line2', type: 'add', newLine: 1 }, 177 | ]); 178 | }); 179 | 180 | it('should parse gnu sample diff', () => { 181 | const diff = ` 182 | --- lao 2002-02-21 23:30:39.942229878 -0800 183 | +++ tzu 2002-02-21 23:30:50.442260588 -0800 184 | @@ -1,7 +1,6 @@ 185 | -The Way that can be told of is not the eternal Way; 186 | -The name that can be named is not the eternal name. 187 | The Nameless is the origin of Heaven and Earth; 188 | -The Named is the mother of all things. 189 | +The named is the mother of all things. 190 | + 191 | Therefore let there always be non-being, 192 | so we may see their subtlety, 193 | And let there always be being, 194 | @@ -9,3 +8,6 @@ 195 | The two are the same, 196 | But after they are produced, 197 | they have different names. 198 | +They both may be called deep and profound. 199 | +Deeper and more profound, 200 | +The door of all subtleties! 201 | `; 202 | const files = parse(diff); 203 | expect(files, 'to have length', 1); 204 | 205 | const f = files[0]; 206 | expect(f.from, 'to be', 'lao'); 207 | expect(f.to, 'to be', 'tzu'); 208 | expect(f.chunks, 'to have length', 2); 209 | 210 | const [c1, c2] = f.chunks; 211 | expect(c1, 'to satisfy', { 212 | oldStart: 1, 213 | oldLines: 7, 214 | newStart: 1, 215 | newLines: 6, 216 | }); 217 | expect(c2, 'to satisfy', { 218 | oldStart: 9, 219 | oldLines: 3, 220 | newStart: 8, 221 | newLines: 6, 222 | }); 223 | }); 224 | 225 | it('should parse hg diff output', () => { 226 | const diff = ` 227 | diff -r 82e55d328c8c -r fef857204a0c hello.c 228 | --- a/hello.c Fri Aug 26 01:21:28 2005 -0700 229 | +++ b/hello.c Sat Aug 16 22:05:04 2008 +0200 230 | @@ -11,6 +11,6 @@ 231 | 232 | int main(int argc, char **argv) 233 | { 234 | - printf("hello, world!\n"); 235 | + printf("hello, world!\"); 236 | return 0; 237 | } 238 | `; 239 | const files = parse(diff); 240 | expect(files, 'to have length', 1); 241 | const f = files[0]; 242 | expect(f.chunks[0].content, 'to be', '@@ -11,6 +11,6 @@'); 243 | expect(f.from, 'to be', 'hello.c'); 244 | expect(f.to, 'to be', 'hello.c'); 245 | }); 246 | 247 | it('should parse svn diff output', () => { 248 | const diff = ` 249 | Index: new.txt 250 | =================================================================== 251 | --- new.txt (revision 0) 252 | +++ new.txt (working copy) 253 | @@ -0,0 +1 @@ 254 | +test 255 | Index: text.txt 256 | =================================================================== 257 | --- text.txt (revision 6) 258 | +++ text.txt (working copy) 259 | @@ -1,7 +1,5 @@ 260 | -This part of the 261 | -document has stayed the 262 | -same from version to 263 | -version. It shouldn't 264 | +This is an important 265 | +notice! It shouldn't 266 | be shown if it doesn't 267 | change. Otherwise, that 268 | would not be helping to 269 | `; 270 | const files = parse(diff); 271 | expect(files, 'to have length', 2); 272 | 273 | const f = files[0]; 274 | expect(f, 'to satisfy', { 275 | from: 'new.txt', 276 | to: 'new.txt', 277 | }); 278 | expect(f.chunks[0].changes, 'to have length', 1); 279 | }); 280 | 281 | it('should parse file names for n new empty file', () => { 282 | const diff = ` 283 | diff --git a/newFile.txt b/newFile.txt 284 | new file mode 100644 285 | index 0000000..e6a2e28 286 | `; 287 | const files = parse(diff); 288 | expect(files, 'to have length', 1); 289 | const f = files[0]; 290 | expect(f, 'to satisfy', { 291 | from: '/dev/null', 292 | to: 'newFile.txt', 293 | }); 294 | }); 295 | 296 | it('should parse file names for a deleted file', () => { 297 | const diff = ` 298 | diff --git a/deletedFile.txt b/deletedFile.txt 299 | deleted file mode 100644 300 | index e6a2e28..0000000 301 | `; 302 | const files = parse(diff); 303 | expect(files, 'to have length', 1); 304 | expect(files[0], 'to satisfy', { 305 | from: 'deletedFile.txt', 306 | to: '/dev/null', 307 | }); 308 | }); 309 | 310 | it('should detect renamed files', () => { 311 | const diff = ` 312 | diff --git a/bar b/bar 313 | new file mode 100644 314 | index 0000000..4e4b354 315 | --- /dev/null 316 | +++ b/bar 317 | @@ -0,0 +1,2 @@ 318 | +this is a 319 | +sample file 320 | diff --git a/foo b/foo 321 | deleted file mode 100644 322 | index 4e4b354..0000000 323 | --- a/foo 324 | +++ /dev/null 325 | @@ -1,2 +0,0 @@ 326 | -this is a 327 | -sample file 328 | `; 329 | 330 | const files = parse(diff, { findRenames: true }); 331 | expect(files, 'to equal', [{ 332 | renamed: true, 333 | from: 'foo', 334 | to: 'bar', 335 | }]); 336 | }); 337 | 338 | 339 | it('should parse line numbers for a file with a single hunk', () => { 340 | const diff = ` 341 | diff --git a/js/foo.js b/js/foo.js 342 | index b2c7faf..2ee2ba2 100644 343 | --- a/js/foo.js 344 | +++ b/js/foo.js 345 | @@ -36,6 +36,7 @@ export type SomeContext = { 346 | foo: bar, 347 | }; 348 | 349 | +import newdep from 'newdep'; 350 | import {bla} from 'bla'; 351 | import {qwe} from 'qwe'; 352 | import {ertyu} from 'ertyu'; 353 | `; 354 | 355 | const files = parse(diff); 356 | const f1 = files[0]; 357 | const c1 = f1.chunks[0]; 358 | expect(c1.changes, 'to satisfy', [ 359 | { content: ' foo: bar,', type: 'normal', position: 1, oldLine: 36, newLine: 36 }, 360 | { content: ' };', type: 'normal', position: 2, oldLine: 37, newLine: 37 }, 361 | { content: ' ', type: 'normal', position: 3, oldLine: 38, newLine: 38 }, 362 | { content: '+import newdep from \'newdep\';', type: 'add', position: 4, newLine: 39 }, 363 | { content: ' import {bla} from \'bla\';', type: 'normal', position: 5, oldLine: 39, newLine: 40 }, 364 | { content: ' import {qwe} from \'qwe\';', type: 'normal', position: 6, oldLine: 40, newLine: 41 }, 365 | { content: ' import {ertyu} from \'ertyu\';', type: 'normal', position: 7, oldLine: 41, newLine: 42 }, 366 | ]); 367 | }); 368 | 369 | it('should parse line numbers for a file with multiple hunks', () => { 370 | const diff = ` 371 | diff --git a/js/model.js b/js/model.js 372 | index 7147fac..1c70551 100644 373 | --- a/js/model.js 374 | +++ b/js/model.js 375 | @@ -17,10 +17,12 @@ export default function Model(storage) { 376 | Model.prototype.create = function create(title = '', callback = () => {}) { 377 | const newItem = { 378 | title: title.trim(), 379 | - completed: false, 380 | - }; 381 | + completed: false 382 | + } 383 | + 384 | + 385 | + 386 | 387 | - this.storage.save(newItem, callback); 388 | }; 389 | 390 | /** 391 | @@ -92,7 +94,6 @@ Model.prototype.getCount = function getCount(callback) { 392 | completed: 0, 393 | total: 0, 394 | }; 395 | - 396 | this.storage.findAll((data) => { 397 | data.forEach((todo) => { 398 | if (todo.completed) { 399 | `; 400 | 401 | const files = parse(diff); 402 | const f1 = files[0]; 403 | const c1 = f1.chunks[0]; 404 | const c2 = f1.chunks[1]; 405 | 406 | expect(c1.changes, 'to contain', { content: '- };', type: 'del', del: true, position: 5, oldLine: 21 }); 407 | expect(c1.changes, 'to contain', { content: '+ }', type: 'add', add: true, position: 7, newLine: 21 }); 408 | expect(c1.changes, 'to contain', { content: '- this.storage.save(newItem, callback);', type: 'del', del: true, position: 12, oldLine: 23 }); 409 | expect(c1.changes, 'to contain', { content: ' /**', type: 'normal', normal: true, position: 15, oldLine: 26, newLine: 28 }); 410 | 411 | expect(c2.changes, 'to contain', { content: '-', type: 'del', del: true, position: 19, oldLine: 95 }); 412 | expect(c2.changes, 'to contain', { content: ' if (todo.completed) {', type: 'normal', normal: true, position: 22, oldLine: 98, newLine: 99 }); 413 | }); 414 | 415 | }); 416 | 417 | --------------------------------------------------------------------------------