diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..cce142ce81 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +_data/pixels.json merge=pixels \ No newline at end of file diff --git a/__tests__/scripts/mergePixels.test.js b/__tests__/scripts/mergePixels.test.js new file mode 100644 index 0000000000..5e628defc3 --- /dev/null +++ b/__tests__/scripts/mergePixels.test.js @@ -0,0 +1,570 @@ +const mockFs = require('mock-fs'); +const fs = require('fs'); +const { promisify } = require('util'); +const { stripIndent } = require('common-tags'); +const { + getSortedPixelsFromFile, + findNewPixel, + getNextCoordinate, + getAlternativePixel, + isPixelTaken, + run +} = require('../../scripts/mergePixels'); + +const readFile = promisify(fs.readFile); + +describe('getSortedPixelsFromFile', () => { + beforeEach(() => { + mockFs({ + '/sorted/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"} + ] + } + `, + '/unsorted/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 6, "x": 2, "color": "#000000", "username": "alex-owl"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"} + ] + } + ` + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + test('returns already sorted ', async () => { + const result = await getSortedPixelsFromFile('/sorted/_data/pixels.json'); + expect(result).toEqual({ + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 1, x: 1, color: '#000000', username: 'sendgrid' } + ] + }); + }); + + test('sorts file if necessary ', async () => { + const result = await getSortedPixelsFromFile('/unsorted/_data/pixels.json'); + expect(result).toEqual({ + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 1, x: 1, color: '#000000', username: 'sendgrid' }, + { y: 6, x: 2, color: '#000000', username: 'alex-owl' } + ] + }); + }); +}); + +describe('findNewPixel', () => { + test('returns the right pixel', () => { + const oldPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 1, x: 1, color: '#000000', username: 'sendgrid' } + ] + }; + + const branchPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 1, x: 1, color: '#000000', username: 'sendgrid' }, + { y: 6, x: 2, color: '#000000', username: 'alex-owl' } + ] + }; + + const result = findNewPixel(oldPixels, branchPixels); + expect(result).toEqual({ + y: 6, + x: 2, + color: '#000000', + username: 'alex-owl' + }); + }); + + test('returns undefined if no pixel is new', () => { + const oldPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 1, x: 1, color: '#000000', username: 'sendgrid' } + ] + }; + + const branchPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 1, x: 1, color: '#000000', username: 'sendgrid' } + ] + }; + + const result = findNewPixel(oldPixels, branchPixels); + expect(result).toBeUndefined(); + }); + + test('returns undefined if branch has less pixels and no new one', () => { + const oldPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 1, x: 1, color: '#000000', username: 'sendgrid' } + ] + }; + + const branchPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' } + ] + }; + + const result = findNewPixel(oldPixels, branchPixels); + expect(result).toBeUndefined(); + }); + + test('throws an error if more than one pixel has been added', () => { + const oldPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 5, x: 3, color: '#000000', username: 'sendgrid' } + ] + }; + + const branchPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 2, x: 2, color: '#000000', username: 'panda' }, + { y: 5, x: 3, color: '#000000', username: 'sendgrid' }, + { y: 6, x: 2, color: '#000000', username: 'alex-owl' } + ] + }; + + const result = () => findNewPixel(oldPixels, branchPixels); + expect(result).toThrowError(); + }); + + test('throws an error if more than one pixel have been touched', () => { + const oldPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 5, x: 3, color: '#000000', username: 'sendgrid' } + ] + }; + + const branchPixels = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 3, x: 3, color: '#000000', username: 'sendgrid' }, + { y: 6, x: 2, color: '#000000', username: 'alex-owl' } + ] + }; + + const result = () => findNewPixel(oldPixels, branchPixels); + expect(result).toThrowError(); + }); +}); + +describe('getNextCoordinate', () => { + test('increase x first if possible', () => { + const { x, y } = getNextCoordinate(0, 0, 5, 5); + expect(x).toBe(1); + expect(y).toBe(0); + }); + + test('increase y and wrap x if limit of width is reached', () => { + const { x, y } = getNextCoordinate(4, 0, 5, 5); + expect(x).toBe(0); + expect(y).toBe(1); + }); + + test('wrap both if limit of width and height is reached', () => { + const { x, y } = getNextCoordinate(4, 4, 5, 5); + expect(x).toBe(0); + expect(y).toBe(0); + }); +}); + +describe('getAlternativePixel', () => { + test('wraps pixel coordinates successfully', () => { + const existing = { + data: [ + { y: 4, x: 2, color: '#000000', username: 'dkundel' }, + { y: 4, x: 3, color: '#000000', username: 'twilio-labs' }, + { y: 4, x: 4, color: '#000000', username: 'twilio' } + ] + }; + const newPixel = { + y: 4, + x: 3, + color: '#f0f0f0', + username: 'panda' + }; + const result = getAlternativePixel(existing, newPixel, { + width: 5, + height: 5 + }); + expect(result).toEqual({ + y: 0, + x: 0, + color: '#f0f0f0', + username: 'panda' + }); + }); + + test('finds the next gap prioritizing x', () => { + const existing = { + data: [ + { y: 4, x: 1, color: '#000000', username: 'dkundel' }, + { y: 4, x: 3, color: '#000000', username: 'twilio-labs' }, + { y: 4, x: 4, color: '#000000', username: 'twilio' } + ] + }; + const newPixel = { + y: 4, + x: 1, + color: '#f0f0f0', + username: 'panda' + }; + const result = getAlternativePixel(existing, newPixel, { + width: 5, + height: 5 + }); + expect(result).toEqual({ + y: 4, + x: 2, + color: '#f0f0f0', + username: 'panda' + }); + }); + + test('changes y if necessary', () => { + const existing = { + data: [ + { y: 3, x: 2, color: '#000000', username: 'dkundel' }, + { y: 3, x: 3, color: '#000000', username: 'twilio-labs' }, + { y: 3, x: 4, color: '#000000', username: 'twilio' } + ] + }; + const newPixel = { + y: 3, + x: 2, + color: '#f0f0f0', + username: 'panda' + }; + const result = getAlternativePixel(existing, newPixel, { + width: 5, + height: 5 + }); + expect(result).toEqual({ + y: 4, + x: 0, + color: '#f0f0f0', + username: 'panda' + }); + }); + + test('throws if no pixel is available', () => { + const existing = { + data: [ + { y: 0, x: 0, color: '#000000', username: 'dkundel' }, + { y: 0, x: 1, color: '#000000', username: 'twilio-labs' }, + { y: 1, x: 0, color: '#000000', username: 'twilio' }, + { y: 1, x: 1, color: '#000000', username: 'sendgrid' } + ] + }; + const newPixel = { + y: 0, + x: 1, + color: '#f0f0f0', + username: 'panda' + }; + + const testFn = () => { + return getAlternativePixel(existing, newPixel, { + width: 2, + height: 2 + }); + }; + expect(testFn).toThrowError(); + }); +}); + +describe('isPixelTaken', () => { + test('returns true if x and y coordinate are taken', () => { + const existing = { + data: [ + { y: 4, x: 2, color: '#000000', username: 'dkundel' }, + { y: 4, x: 3, color: '#000000', username: 'twilio-labs' }, + { y: 4, x: 4, color: '#000000', username: 'twilio' } + ] + }; + const newPixel = { + y: 4, + x: 3, + color: '#f0f0f0', + username: 'panda' + }; + const result = isPixelTaken(existing, newPixel); + expect(result).toBeTruthy(); + }); + + test('returns false if x coordinate is free', () => { + const existing = { + data: [ + { y: 4, x: 2, color: '#000000', username: 'dkundel' }, + { y: 4, x: 4, color: '#000000', username: 'twilio' } + ] + }; + const newPixel = { + y: 4, + x: 3, + color: '#f0f0f0', + username: 'panda' + }; + const result = isPixelTaken(existing, newPixel); + expect(result).toBeFalsy(); + }); + + test('returns false if x coordinate is free', () => { + const existing = { + data: [ + { y: 3, x: 3, color: '#000000', username: 'twilio-labs' }, + { y: 4, x: 2, color: '#000000', username: 'dkundel' }, + { y: 4, x: 4, color: '#000000', username: 'twilio' } + ] + }; + const newPixel = { + y: 4, + x: 3, + color: '#f0f0f0', + username: 'panda' + }; + const result = isPixelTaken(existing, newPixel); + expect(result).toBeFalsy(); + }); +}); + +describe('run', () => { + let consoleWarnBackup = console.warn; + beforeEach(() => { + console.warn = () => {}; + mockFs({ + '/same-pixel-conflict/old/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"} + ] + } + `, + '/same-pixel-conflict/branch/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 6, "x": 2, "color": "#000000", "username": "alex-owl"}, + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"} + ] + } + `, + '/same-pixel-conflict/current/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"}, + {"y": 6, "x": 2, "color": "#000000", "username": "panda"} + ] + } + `, + '/different-pixel-conflict/old/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"} + ] + } + `, + '/different-pixel-conflict/branch/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"}, + {"y": 5, "x": 1, "color": "#000000", "username": "alex-owl"} + ] + } + `, + '/different-pixel-conflict/current/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"}, + {"y": 6, "x": 2, "color": "#000000", "username": "panda"} + ] + } + `, + '/reorder-pixel-conflict/old/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"} + ] + } + `, + '/reorder-pixel-conflict/branch/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"}, + {"y": 5, "x": 1, "color": "#000000", "username": "alex-owl"} + ] + } + `, + '/reorder-pixel-conflict/current/_data/pixels.json': stripIndent` + { + "data": [ + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"}, + {"y": 6, "x": 2, "color": "#000000", "username": "panda"} + ] + } + ` + }); + }); + + afterEach(() => { + console.warn = consoleWarnBackup; + mockFs.restore(); + }); + + test('handles conflict with same coordinates by shifting branch contribution', async () => { + const args = [ + '/some/path/to/node', + '/path/to/scripts/mergePixels.js', + '/same-pixel-conflict/old/_data/pixels.json', + '/same-pixel-conflict/branch/_data/pixels.json', + '/same-pixel-conflict/current/_data/pixels.json' + ]; + + await run(args); + const content = await readFile( + '/same-pixel-conflict/branch/_data/pixels.json', + 'utf8' + ); + expect(content).toBe(stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"}, + {"y": 6, "x": 2, "color": "#000000", "username": "panda"}, + {"y": 6, "x": 3, "color": "#000000", "username": "alex-owl"} + ] + } + `); + }); + + test('handles conflict with different pixels by merging', async () => { + const args = [ + '/some/path/to/node', + '/path/to/scripts/mergePixels.js', + '/different-pixel-conflict/old/_data/pixels.json', + '/different-pixel-conflict/branch/_data/pixels.json', + '/different-pixel-conflict/current/_data/pixels.json' + ]; + + await run(args); + const content = await readFile( + '/different-pixel-conflict/branch/_data/pixels.json', + 'utf8' + ); + expect(content).toBe(stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"}, + {"y": 5, "x": 1, "color": "#000000", "username": "alex-owl"}, + {"y": 6, "x": 2, "color": "#000000", "username": "panda"} + ] + } + `); + }); + + test('handles conflict with reordering changes', async () => { + const args = [ + '/some/path/to/node', + '/path/to/scripts/mergePixels.js', + '/reorder-pixel-conflict/old/_data/pixels.json', + '/reorder-pixel-conflict/branch/_data/pixels.json', + '/reorder-pixel-conflict/current/_data/pixels.json' + ]; + + await run(args); + const content = await readFile( + '/reorder-pixel-conflict/branch/_data/pixels.json', + 'utf8' + ); + expect(content).toBe(stripIndent` + { + "data": [ + {"y": 0, "x": 0, "color": "#000000", "username": "dkundel"}, + {"y": 0, "x": 1, "color": "#000000", "username": "twilio-labs"}, + {"y": 1, "x": 0, "color": "#000000", "username": "twilio"}, + {"y": 1, "x": 1, "color": "#000000", "username": "sendgrid"}, + {"y": 5, "x": 1, "color": "#000000", "username": "alex-owl"}, + {"y": 6, "x": 2, "color": "#000000", "username": "panda"} + ] + } + `); + }); +}); diff --git a/__tests__/utils/pixels-helper.test.js b/__tests__/utils/pixels-helper.test.js new file mode 100644 index 0000000000..3c8e72984c --- /dev/null +++ b/__tests__/utils/pixels-helper.test.js @@ -0,0 +1,117 @@ +const { stripIndent } = require('common-tags'); +const { + sortPixels, + pixelSortFunction, + pixelsToString +} = require('../../utils/pixels-helper'); + +describe('sortPixels', () => { + test('sorts pixels in y ascending order', () => { + const old = { + data: [ + { y: 1, x: 2, color: '#000000', username: 'dkundel' }, + { y: 2, x: 2, color: '#000001', username: 'twilio' }, + { y: 1, x: 3, color: '#000002', username: 'twilio-labs' }, + { y: 0, x: 1, color: '#000002', username: 'panda' } + ] + }; + + const sorted = sortPixels(old); + expect(sorted).toEqual({ + data: [ + { y: 0, x: 1, color: '#000002', username: 'panda' }, + { y: 1, x: 2, color: '#000000', username: 'dkundel' }, + { y: 1, x: 3, color: '#000002', username: 'twilio-labs' }, + { y: 2, x: 2, color: '#000001', username: 'twilio' } + ] + }); + expect(old).toEqual({ + data: [ + { y: 1, x: 2, color: '#000000', username: 'dkundel' }, + { y: 2, x: 2, color: '#000001', username: 'twilio' }, + { y: 1, x: 3, color: '#000002', username: 'twilio-labs' }, + { y: 0, x: 1, color: '#000002', username: 'panda' } + ] + }); + }); + + test('sorts pixels in x ascending order for equal y', () => { + const old = { + data: [ + { y: 1, x: 4, color: '#000000', username: 'dkundel' }, + { y: 1, x: 1, color: '#000001', username: 'twilio' }, + { y: 1, x: 2, color: '#000002', username: 'twilio-labs' }, + { y: 1, x: 3, color: '#000002', username: 'panda' } + ] + }; + + const sorted = sortPixels(old); + expect(sorted).toEqual({ + data: [ + { y: 1, x: 1, color: '#000001', username: 'twilio' }, + { y: 1, x: 2, color: '#000002', username: 'twilio-labs' }, + { y: 1, x: 3, color: '#000002', username: 'panda' }, + { y: 1, x: 4, color: '#000000', username: 'dkundel' } + ] + }); + expect(old).toEqual({ + data: [ + { y: 1, x: 4, color: '#000000', username: 'dkundel' }, + { y: 1, x: 1, color: '#000001', username: 'twilio' }, + { y: 1, x: 2, color: '#000002', username: 'twilio-labs' }, + { y: 1, x: 3, color: '#000002', username: 'panda' } + ] + }); + }); +}); + +describe('pixelSortFunction', () => { + test('sorts by x if y is the same', () => { + const a = { y: 0, x: 0 }; + const b = { y: 0, x: 1 }; + + expect(pixelSortFunction(a, b)).toEqual(-1); + expect(pixelSortFunction(b, a)).toEqual(1); + }); + + test('sorts by y first', () => { + const a = { y: 1, x: 0 }; + const b = { y: 0, x: 1 }; + + expect(pixelSortFunction(a, b)).toEqual(1); + expect(pixelSortFunction(b, a)).toEqual(-1); + }); + + test('returns zero for equal coordinates', () => { + const a = { y: 1, x: 1 }; + const b = { y: 1, x: 1 }; + + expect(pixelSortFunction(a, b)).toEqual(0); + expect(pixelSortFunction(b, a)).toEqual(0); + }); +}); + +describe('pixelsToString', () => { + test('formats the pixels correctly', () => { + const pixels = { + data: [ + { y: 1, x: 1, color: '#000001', username: 'twilio' }, + { y: 1, x: 2, color: '#000002', username: 'twilio-labs' }, + { y: 1, x: 3, color: '#000002', username: 'panda' }, + { y: 1, x: 4, color: '#000000', username: 'dkundel' } + ] + }; + + const pixelString = pixelsToString(pixels); + expect(pixelString).toEqual(stripIndent` + { + "data": [ + {"y": 1, "x": 1, "color": "#000001", "username": "twilio"}, + {"y": 1, "x": 2, "color": "#000002", "username": "twilio-labs"}, + {"y": 1, "x": 3, "color": "#000002", "username": "panda"}, + {"y": 1, "x": 4, "color": "#000000", "username": "dkundel"} + ] + } + `); + }); +}); diff --git a/_data/pixels.json b/_data/pixels.json index be697851c1..971efbae3b 100644 --- a/_data/pixels.json +++ b/_data/pixels.json @@ -684,4 +684,4 @@ {"y": 39.1, "x": 39.1, "color": "#ae2654", "username": "ManiPandi"}, {"y": 39.2, "x": 39.2, "color": "#7132bd", "username": "JISSJOHNSON"} ] -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 653c6c4676..27ee67fa49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1865,6 +1865,12 @@ "restore-cursor": "^2.0.0" } }, + "cli-spinners": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.2.0.tgz", + "integrity": "sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==", + "dev": true + }, "cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -1886,6 +1892,12 @@ "wrap-ansi": "^2.0.0" } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2273,6 +2285,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -2883,18 +2904,84 @@ "dev": true }, "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", + "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", "dev": true, "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^3.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "npm-run-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", + "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, + "path-key": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz", + "integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.1.tgz", + "integrity": "sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "exit": { @@ -3968,9 +4055,9 @@ "dev": true }, "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", "dev": true, "requires": { "pump": "^3.0.0" @@ -4424,6 +4511,21 @@ "slash": "^3.0.0" }, "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -4440,6 +4542,15 @@ "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", "dev": true }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4784,6 +4895,12 @@ "is-extglob": "^2.1.1" } }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -5240,6 +5357,32 @@ "@jest/types": "^24.9.0", "execa": "^1.0.0", "throat": "^4.0.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } } }, "jest-config": { @@ -7147,6 +7290,12 @@ } } }, + "mock-fs": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.10.1.tgz", + "integrity": "sha512-w22rOL5ZYu6HbUehB5deurghGM0hS/xBVyHMGKOuQctkk93J9z9VEOhDsiWrXOprVNQpP9uzGKdl8v9mFspKuw==", + "dev": true + }, "module-definition": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-3.2.0.tgz", @@ -7707,6 +7856,57 @@ "wordwrap": "~1.0.0" } }, + "ora": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/ora/-/ora-4.0.2.tgz", + "integrity": "sha512-YUOZbamht5mfLxPmk4M35CD/5DuOkAacxlEUbStVXpBAt4fyhBf+vZHI/HRkI++QUp3sNoeA2Gw4C+hi4eGSig==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", @@ -8920,6 +9120,32 @@ "micromatch": "^3.1.4", "minimist": "^1.1.1", "walker": "~1.0.5" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } } }, "sass-lookup": { @@ -10414,6 +10640,15 @@ "makeerror": "1.0.x" } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -10474,6 +10709,32 @@ "dev": true, "requires": { "execa": "^1.0.0" + }, + "dependencies": { + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } } }, "with": { diff --git a/package.json b/package.json index 18c952e51a..171287bb73 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,16 @@ "private": true, "description": "A project to teach people how to do their first pull request", "scripts": { + "configure:merge-driver": "node scripts/configureMergeDriver.js", + "sync-fork": "node scripts/sync.js", + "format:sort-pixels": "node scripts/sortPixels.js", "lint-staged": "lint-staged", "start": "eleventy --serve", "build": "eleventy", "prettier": "prettier --write \"**/*.js\"", - "test": "jest" + "test": "jest", + "test:pixeldata": "jest data.test.js", + "postinstall": "npm run configure:merge-driver" }, "repository": { "type": "git", @@ -27,11 +32,14 @@ "common-tags": "^1.8.0", "danger": "^9.2.0", "dot-prop": "^5.1.0", + "execa": "^2.1.0", "husky": "^3.0.8", "jest": "^24.9.0", "json-stringify-pretty-compact": "^2.0.0", "lint-staged": "^9.4.1", + "mock-fs": "^4.10.1", "npm-run-all": "^4.1.5", + "ora": "^4.0.2", "prettier": "^1.18.2" }, "dependencies": { @@ -39,17 +47,19 @@ }, "husky": { "hooks": { - "pre-commit": "run-s lint-staged test" + "pre-commit": "run-s lint-staged" } }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ "prettier --write", - "git add" + "git add", + "jest --findRelatedTests" ], "_data/pixels.json": [ - "node tasks/sortPixels.js", - "git add" + "node scripts/sortPixels.js", + "git add", + "npm run test:pixeldata" ] } } diff --git a/scripts/configureMergeDriver.js b/scripts/configureMergeDriver.js new file mode 100755 index 0000000000..eda12e6e34 --- /dev/null +++ b/scripts/configureMergeDriver.js @@ -0,0 +1,24 @@ +const { command: exec } = require('execa'); +const ora = require('ora'); + +let spinner; +async function run() { + spinner = ora('Configuring custom merge driver').start(); + await exec( + `git config merge.pixels.name "A custom merge driver for the pixels file"`, + { shell: true } + ); + await exec( + `git config merge.pixels.driver "node scripts/mergePixels.js %O %A %B"`, + { shell: true } + ); + spinner.succeed('Configured custom merge driver'); +} + +run().catch(err => { + if (spinner) { + spinner.fail('Failed to configure custom merge driver for pixels.json'); + } + console.error(err.all); + process.exit(1); +}); diff --git a/scripts/mergePixels.js b/scripts/mergePixels.js new file mode 100644 index 0000000000..8af6cb14a0 --- /dev/null +++ b/scripts/mergePixels.js @@ -0,0 +1,180 @@ +const fs = require('fs'); +const { stripIndent } = require('common-tags'); +const path = require('path'); +const { promisify } = require('util'); +const { sortPixels, pixelsToString } = require('../utils/pixels-helper'); +const appDefaults = require('../_data/defaults.json'); + +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +const pixelToId = pixel => `${pixel.x}|${pixel.y}|${pixel.username}`; +const coordinatesToId = coord => `${coord.x},${coord.y}`; + +async function getSortedPixelsFromFile(filePath) { + const fileContent = await readFile(filePath, 'utf8'); + const unsortedPixelsJson = JSON.parse(fileContent); + return sortPixels(unsortedPixelsJson); +} + +function findNewPixel(oldPixels, branchPixels) { + const existingPixels = new Set(oldPixels.data.map(pixelToId)); + + const missingPixels = branchPixels.data.filter( + pixel => !existingPixels.has(pixelToId(pixel)) + ); + + if (missingPixels.length > 1) { + const msg = ` +More pixels than one have been added or modified. This requires a manual merge. + +The following pixels have been added or modified: + +${pixelsToString(missingPixels)} + +We are overriding the changes with the latest changes and wrote the conflicting pixels into a file in the project called "pixels-conflict.log" + `.trim(); + + const err = new Error(msg); + Object.defineProperty(err, 'missingPixels', { value: missingPixels }); + throw err; + } + + return missingPixels[0]; +} + +function getNextCoordinate(currentX, currentY, width, height) { + let x = currentX + 1; + let y = currentY; + + if (x >= width) { + x = 0; + } + + if (x === 0) { + y = currentY + 1; + + if (y >= height) { + y = 0; + } + } + + return { x, y }; +} + +function getAlternativePixel(takenPixels, invalidPixel, image) { + const { username, x: preferredX, y: preferredY, color } = invalidPixel; + + const takenCoordinates = new Set(takenPixels.data.map(coordinatesToId)); + + let x = preferredX; + let y = preferredY; + + do { + const { x: newX, y: newY } = getNextCoordinate( + x, + y, + image.width, + image.height + ); + + if (newX === preferredX && newY === preferredY) { + throw new Error( + 'Could not find any free pixel. Please file an issue about increasing the canvas size.' + ); + } else { + x = newX; + y = newY; + } + } while (takenCoordinates.has(coordinatesToId({ x, y }))); + + return { y, x, color, username }; +} + +function isPixelTaken(currentPixels, newPixel) { + return ( + currentPixels.data.find( + pixel => pixel.x === newPixel.x && pixel.y === newPixel.y + ) !== undefined + ); +} + +async function run(args) { + const [ + nodePath, + scriptPath, + oldFilePath, + branchFilePath, + currentFilePath + ] = args; + + const oldPixels = await getSortedPixelsFromFile(oldFilePath); + const currentPixels = await getSortedPixelsFromFile(currentFilePath); + const branchPixels = await getSortedPixelsFromFile(branchFilePath); + + let newPixel; + try { + newPixel = findNewPixel(oldPixels, branchPixels); + } catch (err) { + if (err.missingPixels) { + const content = ` +The following pixels were removed from the _data/pixels.json file due to a conflict. +If this was by accident please place them back into the right location and commit again. + +${pixelsToString({ data: err.missingPixels })} + `.trim(); + await writeFile( + path.resolve(__dirname, '../pixels-conflict.log'), + content, + 'utf8' + ); + } + const outputPixels = sortPixels(currentPixels); + await writeFile(branchFilePath, pixelsToString(outputPixels), 'utf8'); + + throw err; + } + + const pixelIsTaken = isPixelTaken(currentPixels, newPixel); + if (newPixel && !pixelIsTaken) { + currentPixels.data.push(newPixel); + } else if (newPixel) { + const alternativePixel = getAlternativePixel( + currentPixels, + newPixel, + appDefaults.image + ); + + console.warn( + ` +Unfortunately your pixel already had been taken. Instead we picked the following pixel for you: + +${pixelsToString(alternativePixel)} + +If you do not like this pixel, feel free to pick another one instead by modifying the file again and commiting the new changes. + `.trim() + ); + currentPixels.data.push(alternativePixel); + } + + const outputPixels = sortPixels(currentPixels); + await writeFile(branchFilePath, pixelsToString(outputPixels), 'utf8'); +} + +if (process.argv.length >= 5 && process.argv[1].includes('mergePixels.js')) { + run(process.argv) + .then(() => process.exit(0)) + .catch(err => { + console.error(err.message); + process.exit(1); + }); +} + +module.exports = { + getSortedPixelsFromFile, + findNewPixel, + getNextCoordinate, + getAlternativePixel, + isPixelTaken, + run +}; diff --git a/scripts/sortPixels.js b/scripts/sortPixels.js new file mode 100644 index 0000000000..dddafee104 --- /dev/null +++ b/scripts/sortPixels.js @@ -0,0 +1,19 @@ +const fs = require('fs'); +const path = require('path'); +const { promisify } = require('util'); +const { sortPixels, pixelsToString } = require('../utils/pixels-helper'); + +const readFile = promisify(fs.readFile); +const writeFile = promisify(fs.writeFile); + +const pixelFilePath = path.join('_data', 'pixels.json'); + +readFile(pixelFilePath, { encoding: 'utf8' }) + .then(pixelFileData => { + const pixels = JSON.parse(pixelFileData); + + const sortedPixelString = pixelsToString(sortPixels(pixels)); + + writeFile(pixelFilePath, sortedPixelString); + }) + .catch(console.log); diff --git a/scripts/sync.js b/scripts/sync.js new file mode 100755 index 0000000000..2863ea5ee7 --- /dev/null +++ b/scripts/sync.js @@ -0,0 +1,78 @@ +const { command: exec } = require('execa'); +const ora = require('ora'); +const os = require('os'); + +const trim = x => x.trim(); + +const opts = { + shell: true +}; + +async function getBranch() { + const { stdout: branches } = await exec('git branch', opts); + const branch = branches + .split(os.EOL) + .map(trim) + .find(x => x.startsWith('*')); + if (!branch) { + throw new Error('Failed to determine current branch'); + } + + return branch.replace('* ', ''); +} + +let spinner; + +async function run() { + spinner = ora('Syncing your fork with the original repo').start(); + + spinner.text = 'Getting current branch'; + const currentBranch = await getBranch(); + + spinner.text = 'Checking if remote "twilio-labs" is configured'; + const { stdout: remotes } = await exec('git remote', opts); + const twilioLabsRemoteExists = remotes + .split(os.EOL) + .map(trim) + .includes('twilio-labs'); + + if (!twilioLabsRemoteExists) { + spinner.text = 'Configuring "twilio-labs" remote'; + await exec( + 'git remote add twilio-labs https://github.com/twilio-labs/open-pixel-art.git', + opts + ); + } + + spinner.text = 'Checking out master branch'; + await exec('git checkout master', opts); + + spinner.text = 'Pulling latest changes for master'; + await exec('git pull twilio-labs master', opts); + + spinner.text = 'Pushing changes up to your fork (origin)'; + await exec('git push origin master', opts); + + spinner.text = 'Pushing changes up to your fork (origin)'; + await exec('git push origin master', opts); + + spinner.text = `Checking out "${currentBranch}" branch`; + await exec(`git checkout ${currentBranch}`, opts); + + spinner.text = `Merging in changes from master to ${currentBranch}`; + spinner.stopAndPersist(); + await exec('git merge master -m "chore: merge changes from master"', { + ...opts, + stdio: 'inherit' + }); + + spinner.succeed('Project has been successfully updated'); +} + +run().catch(err => { + if (spinner) { + spinner.fail('Failed to sync'); + } + console.error(err.all); + process.exit(1); +}); diff --git a/tasks/sortPixels.js b/tasks/sortPixels.js deleted file mode 100644 index 516fb645c5..0000000000 --- a/tasks/sortPixels.js +++ /dev/null @@ -1,49 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { promisify } = require('util'); -const stringify = require('json-stringify-pretty-compact'); - -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); - -const pixelFilePath = path.join('_data', 'pixels.json'); - -console.log(pixelFilePath); - -readFile(pixelFilePath, { encoding: 'utf8' }) - .then(pixelFileData => { - const pixels = JSON.parse(pixelFileData); - - pixels.data.sort(sortPixels); - const sortedPixelString = stringify(pixels, { indent: 2, maxLength: 100 }); - - writeFile(pixelFilePath, sortedPixelString); - }) - .catch(console.log); - -function sortPixels(a, b) { - const xDiff = a.x - b.x; - const yDiff = a.y - b.y; - - if (yDiff > 0) { - // pixel is further down a row - return 1; - } - - if (yDiff < 0) { - // pixel a closer to the beginning of a row - return -1; - } - - // yDiff must be 0 to make it here - - if (xDiff > 0) { - // pixel a is in a lower row - return 1; - } - - if (xDiff < 0) { - // pixel a is in a higher row - return -1; - } -} diff --git a/utils/pixels-helper.js b/utils/pixels-helper.js new file mode 100644 index 0000000000..cca98efd6f --- /dev/null +++ b/utils/pixels-helper.js @@ -0,0 +1,48 @@ +const stringify = require('json-stringify-pretty-compact'); + +function pixelSortFunction(a, b) { + const xDiff = a.x - b.x; + const yDiff = a.y - b.y; + + if (yDiff > 0) { + // pixel is further down a row + return 1; + } + + if (yDiff < 0) { + // pixel a closer to the beginning of a row + return -1; + } + + // yDiff must be 0 to make it here + + if (xDiff > 0) { + // pixel a is in a lower row + return 1; + } + + if (xDiff < 0) { + // pixel a is in a higher row + return -1; + } + + return 0; +} + +function sortPixels(pixelJson) { + const data = [...pixelJson.data].sort(pixelSortFunction); + + return { + data + }; +} + +function pixelsToString(pixelJson) { + return stringify(pixelJson, { indent: 2, maxLength: 100 }); +} + +module.exports = { + sortPixels, + pixelSortFunction, + pixelsToString +};