diff --git a/src/github-handler/comment-handler/patch-handler/hunk-to-patch-handler.ts b/src/github-handler/comment-handler/patch-handler/hunk-to-patch-handler.ts new file mode 100644 index 00000000..6e8b0ded --- /dev/null +++ b/src/github-handler/comment-handler/patch-handler/hunk-to-patch-handler.ts @@ -0,0 +1,56 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {Hunk, Patch, RawContent} from '../../../types'; + +/** + * From the each file's hunk, generate each file's hunk's old version range + * and the new content (from the user's) to update that range with. + * @param filesHunks a list of hunks for each file where the lines are at least 1 + * @param rawChanges + * @returns patches to upload to octokit + */ +export function generatePatches( + filesHunks: Map, + rawChanges: Map +): Map { + const filesPatches: Map = new Map(); + filesHunks.forEach((hunks, fileName) => { + const patches: Patch[] = []; + // we expect all hunk lists to not be empty + hunks.forEach(hunk => { + // returns a copy of the hashmap value, then creates a new list with new substrings + const lines = rawChanges.get(fileName)!.newContent.split('\n'); + // creates a new shallow-copied subarray + // we assume the hunk new start and new end to be within the domain of the lines length + if ( + hunk.newStart < 1 || + hunk.newEnd < 1 || + hunk.oldStart < 1 || + hunk.oldEnd < 1 + ) { + throw new RangeError('The file line value should be at least 1'); + } + const newContent = lines.slice(hunk.newStart - 1, hunk.newEnd - 1); + const patch: Patch = { + newContent: newContent.join('\n'), + start: hunk.oldStart, + end: hunk.oldEnd, + }; + patches.push(patch); + }); + filesPatches.set(fileName, patches); + }); + return filesPatches; +} diff --git a/src/github-handler/comment-handler/patch-handler/index.ts b/src/github-handler/comment-handler/patch-handler/index.ts index 8fb99442..67baba90 100644 --- a/src/github-handler/comment-handler/patch-handler/index.ts +++ b/src/github-handler/comment-handler/patch-handler/index.ts @@ -12,8 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +import {generatePatches} from './hunk-to-patch-handler'; import {getValidSuggestionHunks} from './in-scope-hunks-handler'; -import {RawContent, Range, Patch} from '../../../types'; +import {Hunk, RawContent, Range, Patch} from '../../../types'; + +interface SuggestionPatches { + filePatches: Map; + outOfScopeSuggestions: Map; +} /** * Get the range of the old version of every file and the corresponding new text for that range @@ -27,9 +33,15 @@ export function getSuggestionPatches( rawChanges: Map, invalidFiles: string[], validFileLines: Map -): Map { - const filePatches: Map = new Map(); - getValidSuggestionHunks(rawChanges, invalidFiles, validFileLines); - // TODO get patches from getValidSuggestionHunks output - return filePatches; +): SuggestionPatches { + const {inScopeSuggestions, outOfScopeSuggestions} = getValidSuggestionHunks( + rawChanges, + invalidFiles, + validFileLines + ); + const filePatches: Map = generatePatches( + inScopeSuggestions, + rawChanges + ); + return {filePatches, outOfScopeSuggestions}; } diff --git a/test/hunk-to-patch.ts b/test/hunk-to-patch.ts new file mode 100644 index 00000000..eb58480b --- /dev/null +++ b/test/hunk-to-patch.ts @@ -0,0 +1,240 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {expect} from 'chai'; +import {describe, it, before, beforeEach} from 'mocha'; +import {setup} from './util'; +import {generatePatches} from '../src/github-handler/comment-handler/patch-handler/hunk-to-patch-handler'; +import {Hunk, RawContent} from '../src/types'; + +before(() => { + setup(); +}); + +describe('generatePatches', () => { + const rawChanges: Map = new Map(); + const filesHunks: Map = new Map(); + + const fileName1Addition = 'file-1.txt'; + const fileName2Addition = 'file-2.txt'; + const fileNameDelete = 'file-3.txt'; + const fileNameSpecialChar1 = 'file-4.txt'; + const fileNameEmtpy = 'file-5.txt'; + const fileNameNonEmtpy = 'file-6.txt'; + + beforeEach(() => { + rawChanges.clear(); + filesHunks.clear(); + }); + + it('Gets the correct substrings when there is 1 addition', () => { + rawChanges.set(fileName1Addition, { + oldContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + newContent: + 'addition+line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + }); + filesHunks.set(fileName1Addition, [ + {oldStart: 1, oldEnd: 6, newStart: 1, newEnd: 7}, + ]); + const filePatches = generatePatches(filesHunks, rawChanges); + expect(filePatches.get(fileName1Addition)!).deep.equals([ + { + start: 1, + end: 6, + newContent: 'addition+line1\nline2\nline3\nline4\nline5\nline6', + }, + ]); + }); + + it('Gets the correct substrings when there is 2 additions', () => { + rawChanges.set(fileName2Addition, { + oldContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + newContent: + 'line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10+another addition\n', + }); + filesHunks.set(fileName2Addition, [ + {oldStart: 1, oldEnd: 6, newStart: 1, newEnd: 7}, + {oldStart: 9, oldEnd: 12, newStart: 10, newEnd: 13}, + ]); + const filePatches = generatePatches(filesHunks, rawChanges); + expect(filePatches.get(fileName2Addition)!).deep.equals([ + { + start: 1, + end: 6, + newContent: 'line0\nline1\nline2\nline3\nline4\nline5', + }, + { + start: 9, + end: 12, + newContent: 'line9\nline10+another addition\n', + }, + ]); + }); + + it('Gets the correct substrings when there is 1 deletion', () => { + rawChanges.set(fileNameDelete, { + oldContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + newContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\n', + }); + filesHunks.set(fileNameDelete, [ + {oldStart: 9, oldEnd: 12, newStart: 9, newEnd: 11}, + ]); + const filePatches = generatePatches(filesHunks, rawChanges); + expect(filePatches.get(fileNameDelete)!).deep.equals([ + { + start: 9, + end: 12, + newContent: 'line9\n', + }, + ]); + }); + + it('Gets the correct substrings when there is a special patch char prepending the text', () => { + rawChanges.set(fileNameSpecialChar1, { + oldContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + newContent: + '+line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + }); + + filesHunks.set(fileNameSpecialChar1, [ + {oldStart: 1, oldEnd: 2, newStart: 1, newEnd: 2}, + ]); + const filePatches = generatePatches(filesHunks, rawChanges); + expect(filePatches.get(fileNameSpecialChar1)!).deep.equals([ + { + start: 1, + end: 2, + newContent: '+line1', + }, + ]); + }); + + it('Gets the correct substrings when the file is now an empty string', () => { + rawChanges.set(fileNameEmtpy, { + oldContent: 'line1', + newContent: '', + }); + + filesHunks.set(fileNameEmtpy, [ + {oldStart: 1, oldEnd: 2, newStart: 1, newEnd: 1}, + ]); + const filePatches = generatePatches(filesHunks, rawChanges); + expect(filePatches.get(fileNameEmtpy)!).deep.equals([ + { + start: 1, + end: 2, + newContent: '', + }, + ]); + }); + + it('Gets the correct substrings when the empty string file is now has text', () => { + rawChanges.set(fileNameNonEmtpy, { + oldContent: '', + newContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + }); + + filesHunks.set(fileNameNonEmtpy, [ + {oldStart: 1, oldEnd: 1, newStart: 1, newEnd: 12}, + ]); + const filePatches = generatePatches(filesHunks, rawChanges); + expect(filePatches.get(fileNameNonEmtpy)!).deep.equals([ + { + start: 1, + end: 1, + newContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + }, + ]); + }); + + it('Throws an error when the new start hunk line is 0', () => { + rawChanges.set(fileNameNonEmtpy, { + oldContent: '', + newContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + }); + filesHunks.set(fileNameNonEmtpy, [ + {oldStart: 1, oldEnd: 1, newStart: 0, newEnd: 12}, + ]); + try { + generatePatches(filesHunks, rawChanges); + expect.fail( + 'Should have errored because the new start line is < 1. Value should be >= 1' + ); + } catch (err) { + expect(err instanceof RangeError).equals(true); + } + }); + it('Throws an error when the new end hunk line is 0', () => { + rawChanges.set(fileNameNonEmtpy, { + oldContent: '', + newContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + }); + filesHunks.set(fileNameNonEmtpy, [ + {oldStart: 2, oldEnd: 1, newStart: 1, newEnd: 0}, + ]); + try { + generatePatches(filesHunks, rawChanges); + expect.fail( + 'Should have errored because the new end line is < 1. Value should be >= 1' + ); + } catch (err) { + expect(err instanceof RangeError).equals(true); + } + }); + it('Throws an error when the old start hunk line is 0', () => { + rawChanges.set(fileNameNonEmtpy, { + oldContent: '', + newContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + }); + filesHunks.set(fileNameNonEmtpy, [ + {oldStart: 0, oldEnd: 1, newStart: 1, newEnd: 12}, + ]); + try { + generatePatches(filesHunks, rawChanges); + expect.fail( + 'Should have errored because the old start line is < 1. Value should be >= 1' + ); + } catch (err) { + expect(err instanceof RangeError).equals(true); + } + }); + it('Throws an error when theold end hunk line is 0', () => { + rawChanges.set(fileNameNonEmtpy, { + oldContent: '', + newContent: + 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\n', + }); + filesHunks.set(fileNameNonEmtpy, [ + {oldStart: 2, oldEnd: 0, newStart: 1, newEnd: 2}, + ]); + try { + generatePatches(filesHunks, rawChanges); + expect.fail( + 'Should have errored because the old end line is < 1. Value should be >= 1' + ); + } catch (err) { + expect(err instanceof RangeError).equals(true); + } + }); +});