├── .clang-format ├── .gitignore ├── .travis.yml ├── CREDITS.md ├── Dockerfile ├── README.md ├── TODO.md ├── backend ├── package-lock.json ├── package.json ├── run-tests.sh ├── run.sh ├── src │ ├── App.ts │ ├── SharedDiff.ts │ ├── actions │ │ ├── ActionUtils.ts │ │ ├── CreateSharedDiffAction.ts │ │ ├── DeleteSharedDiffAction.ts │ │ ├── ExtendLifetimeSharedDiffAction.ts │ │ ├── GetSharedDiffAction.ts │ │ └── MakePermanentSharedDiffAction.ts │ ├── config.js │ ├── config_dev.js │ ├── crons │ │ └── delete_expired_diffs_cron.ts │ ├── metrics │ │ ├── GAMetrics.ts │ │ ├── LogBasedMetrics.ts │ │ └── Metrics.ts │ ├── scripts │ │ ├── add_created_and_expired_dates_to_rows.js │ │ └── diff_copy_tool.ts │ ├── sharedDiffRepository │ │ ├── DoubleWriteDiffRepository.ts │ │ ├── GoogleDatastoreDiffRepository.ts │ │ ├── MemoryDiffRepository.ts │ │ ├── MongoSharedDiffRepository.ts │ │ └── SharedDiffRepository.ts │ ├── utils.js │ └── utils │ │ └── ConfigFileResolver.ts ├── tests │ ├── MockedMetrics.ts │ ├── SharedDiff.test.ts │ ├── actions │ │ ├── ActionUtils.test.ts │ │ ├── CreateSharedDiffAction.test.ts │ │ ├── DeleteSharedDiffAction.test.ts │ │ ├── ExtendLifetimeSharedDiffAction.test.ts │ │ ├── GetSharedDiffAction.test.ts │ │ └── MakePermanentSharedDiffAction.ts │ ├── config.js │ ├── sharedDiffRepository │ │ ├── MemorySharedDiffRepository.test.ts │ │ ├── MongoSharedDiffRepository.test.ts │ │ └── SharedDiffRepository.test.ts │ └── utils.js └── tsconfig.json ├── docker-compose.yml ├── frontend ├── .editorconfig ├── README.md ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── Alert.ts │ │ ├── alert.service.spec.ts │ │ ├── alert.service.ts │ │ ├── analytics.service.spec.ts │ │ ├── analytics.service.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── diff-detail-content │ │ │ ├── diff-detail-content.component.css │ │ │ ├── diff-detail-content.component.html │ │ │ ├── diff-detail-content.component.spec.ts │ │ │ └── diff-detail-content.component.ts │ │ ├── diff-detail-countdown │ │ │ ├── diff-detail-countdown.component.css │ │ │ ├── diff-detail-countdown.component.html │ │ │ ├── diff-detail-countdown.component.spec.ts │ │ │ └── diff-detail-countdown.component.ts │ │ ├── diff-detail-nav │ │ │ ├── diff-detail-nav.component.css │ │ │ ├── diff-detail-nav.component.html │ │ │ ├── diff-detail-nav.component.spec.ts │ │ │ └── diff-detail-nav.component.ts │ │ ├── diff-detail │ │ │ ├── diff-detail.component.css │ │ │ ├── diff-detail.component.html │ │ │ ├── diff-detail.component.spec.ts │ │ │ ├── diff-detail.component.ts │ │ │ ├── printer-utils.ts │ │ │ └── tree-functions.ts │ │ ├── diff-file-tree │ │ │ ├── diff-file-tree.component.css │ │ │ ├── diff-file-tree.component.html │ │ │ ├── diff-file-tree.component.spec.ts │ │ │ └── diff-file-tree.component.ts │ │ ├── diffy.service.spec.ts │ │ ├── diffy.service.ts │ │ ├── highlight │ │ │ ├── highlight.component.css │ │ │ ├── highlight.component.html │ │ │ ├── highlight.component.spec.ts │ │ │ └── highlight.component.ts │ │ ├── home-page │ │ │ ├── home-page.component.css │ │ │ ├── home-page.component.html │ │ │ ├── home-page.component.spec.ts │ │ │ └── home-page.component.ts │ │ ├── pipes │ │ │ └── keep-html.pipe.ts │ │ └── types │ │ │ └── Error.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── diff.css │ │ ├── diff2html.css │ │ ├── fileTree.css │ │ └── img │ │ │ ├── diffy-logo.gif │ │ │ └── favicon.ico │ ├── browserslist │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── karma.conf.js │ ├── main.ts │ ├── polyfills.ts │ ├── styles.css │ ├── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json └── tslint.json ├── models ├── package-lock.json ├── package.json ├── src │ ├── ActionDefinitions.ts │ ├── index.ts │ ├── io │ │ ├── CreateDiff.ts │ │ ├── DeleteDiff.ts │ │ ├── ExtendDiffLifetime.ts │ │ ├── GetDiff.ts │ │ └── MakeDiffPermanent.ts │ └── models.ts └── tsconfig.json └── tools ├── backend_compile.sh ├── backend_install.sh ├── format_code.sh ├── frontend_compile.sh ├── frontend_install.sh ├── models_compile.sh ├── ng_build_watch.sh ├── rebuild_image.sh └── run_tests.sh /.clang-format: -------------------------------------------------------------------------------- 1 | Language: JavaScript 2 | BasedOnStyle: Google 3 | ColumnLimit: 100 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.swp 3 | .nyc_output/ 4 | .idea/ 5 | build/ 6 | dist/ 7 | node_modules/ 8 | coverage/ 9 | data/ 10 | npm-debug.log 11 | # See http://help.github.com/ignore-files/ for more about ignoring files. 12 | 13 | # compiled output 14 | /dist 15 | /tmp 16 | /out-tsc 17 | 18 | # dependencies 19 | /node_modules 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | .vscode/ 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # misc 39 | /.sass-cache 40 | /connect.lock 41 | /coverage 42 | /libpeerconnection.log 43 | npm-debug.log 44 | yarn-error.log 45 | testem.log 46 | /typings 47 | 48 | # System Files 49 | .DS_Store 50 | Thumbs.db 51 | 52 | playground_scripts/ 53 | deploy_scripts/ 54 | docker/ 55 | kubernetes/ 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | services: 5 | - mongodb 6 | 7 | before_install: 8 | - cd backend/ 9 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | * Paulo Bu (creator) 4 | * [Robin Curbelo](https://github.com/jcurbelo) 5 | * [Oscar Mederos](https://github.com/omederos) 6 | 7 | Thank you very much for the help! :-) 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.7.0 2 | 3 | # Create a /diffy directory that will contain the application's code 4 | RUN mkdir -p /diffy/backend 5 | RUN mkdir -p /diffy/frontend 6 | 7 | RUN npm install -g typescript@4.3.5 --legacy-peer-deps 8 | # Angular stuff (cli and dev) 9 | RUN npm install -g @angular/cli@12.2.2 --legacy-peer-deps 10 | 11 | COPY ./models/ /diffy/models/ 12 | COPY ./backend/ /diffy/backend/ 13 | COPY ./frontend/ /diffy/frontend/ 14 | 15 | # Models 16 | WORKDIR /diffy/models 17 | RUN npm install 18 | RUN npm run-script build 19 | 20 | # Frontend 21 | WORKDIR /diffy/frontend 22 | RUN npm install --legacy-peer-deps 23 | RUN npm run-script build 24 | 25 | # Backend 26 | WORKDIR /diffy/backend 27 | RUN npm install 28 | RUN npm run-script build 29 | 30 | 31 | 32 | # By default expose port 3000 and run `node /diffy/src/app.js` when executing the image 33 | EXPOSE 3000 34 | CMD ["npm", "start"] 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diffy - A tool for sharing diff output online [![Build Status](https://travis-ci.org/pbu88/diffy.svg)](https://travis-ci.org/pbu88/diffy) 2 | 3 | https://diffy.org 4 | 5 | ## How to contribute 6 | 7 | Diffy is a Node.js application. Appart from Node.js, the only other 8 | thing you'll need is mongodb. To get you started these are the steps: 9 | 10 | 1. Install Node.js and NPM 11 | 2. Install MongoDB and make it listen on localhost with default port 12 | 3. Clone the repo: `git clone https://github.com/pbu88/diffy.git` 13 | 4. Install and build frontend code (AngularJS app) 14 | * `cd diffy/frontend` 15 | * `npm install` 16 | * `ng build` 17 | 5. Install and build backend code (Typescript) 18 | * `cd diff/backend/` 19 | * `npm install` 20 | * `npm run build` 21 | * `npm test` 22 | 7. Run it: `DIFFY_GA_ANALYTICS_KEY=none npm run v2_start` 23 | 24 | ### Docker 25 | 26 | If you want to run Diffy using Docker, you don't need to follow any of the above manual steps: 27 | 28 | 1. Install [docker](https://docs.docker.com/engine/installation/) and 29 | [docker-compose](https://docs.docker.com/compose/install/) 30 | 2. Run the tests: `docker-compose run web npm test` 31 | 3. Launch diffy: `docker-compose up` 32 | 33 | The mongodb data will be stored on the `data/` folder. 34 | 35 | That should get you with a basic working dev environment. Now, go ahead 36 | and fill your pull request :) 37 | 38 | Also, feel free to create an issue if you find a bug or if something isn't working as expected when 39 | setting up the development environment. 40 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Things to do 2 | 3 | # NG 4 | * Make sure subfolders keep their open/closed state when parents are toggled 5 | 6 | # Overall 7 | * Unify test frameworks 8 | * Parse commit message and author (to get emails) 9 | * Catch error and exceptions and send emails (maybe pm2 has something already) 10 | * Fix the bad diff that blocks the site 11 | 12 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diffy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.js", 7 | "scripts": { 8 | "test": "sh run-tests.sh", 9 | "build": "rm -r dist;tsc", 10 | "v2_test": "jest", 11 | "start": "sh ./run.sh" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@google-cloud/datastore": "6.5.0", 17 | "body-parser": "^1.13.3", 18 | "cookie-parser": "^1.3.5", 19 | "diff2html": "3.4.9", 20 | "express": "^4.16.2", 21 | "mongodb": "4.1.0", 22 | "promise": "^8.0.0", 23 | "diffy-models" : "file:../models/" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "16.7.1", 27 | "@types/express": "4.17.13", 28 | "@types/hogan.js": "3.0.1", 29 | "@types/jest": "^22.0.1", 30 | "chai": "^4.0.2", 31 | "jest": "^22.1.1", 32 | "mocha": "^3.0.2", 33 | "typescript": "4.3.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/run-tests.sh: -------------------------------------------------------------------------------- 1 | #bash script to build and run tests and set env variables for tests 2 | 3 | npm run build; 4 | export DIFFY_GA_ANALYTICS_KEY='fake key' 5 | mocha dist/tests/ 6 | # the testURL hack is necessary because of: https://github.com/jsdom/jsdom/issues/2304 7 | jest --testURL http://localhost 8 | -------------------------------------------------------------------------------- /backend/run.sh: -------------------------------------------------------------------------------- 1 | node dist/src/App.js 2 | -------------------------------------------------------------------------------- /backend/src/App.ts: -------------------------------------------------------------------------------- 1 | import { GetSharedDiffAction } from './actions/GetSharedDiffAction'; 2 | import { CreateSharedDiffAction } from './actions/CreateSharedDiffAction'; 3 | import { DeleteSharedDiffAction } from './actions/DeleteSharedDiffAction'; 4 | import { ExtendLifetimeSharedDiffAction } from './actions/ExtendLifetimeSharedDiffAction'; 5 | import { ContextParser, CreateDiffInputFactory, DeleteDiffInputFactory, ExtendDiffLifetimeInputFactory, GetDiffInput, GetDiffInputFactory, MakeDiffPermanentInputFactory, SharedDiff } from "diffy-models"; 6 | import { getRepositorySupplierFor } from './sharedDiffRepository/SharedDiffRepository'; 7 | import { GAMetrics } from './metrics/GAMetrics'; 8 | import { toMPromise } from './actions/ActionUtils'; 9 | import { MakePermanentSharedDiffAction } from './actions/MakePermanentSharedDiffAction'; 10 | import { ConfigFileResolver } from './utils/ConfigFileResolver'; 11 | var express = require('express'); 12 | var bodyParser = require('body-parser'); 13 | var cookieParser = require('cookie-parser'); 14 | var path = require('path'); 15 | 16 | const PROJECT_ROOT = path.join(__dirname + '/../../../'); 17 | const STATICS_FOLDER = path.join(PROJECT_ROOT, 'frontend/dist/ngdiffy'); 18 | const INDEX_FILE = path.join(PROJECT_ROOT + '/frontend/dist/ngdiffy/index.html'); 19 | 20 | var app = express(); 21 | 22 | var config_file = ConfigFileResolver.resolve(process.argv, process.env); 23 | console.info(`Using config file: ${config_file}`) 24 | var config = require(config_file); 25 | 26 | const repo = getRepositorySupplierFor(config.DIFF_REPO)(); 27 | 28 | if (!config.GA_ANALITYCS_KEY) { 29 | throw new Error('GA_ANALYTICS_KEY has to be present'); 30 | } 31 | 32 | app.use('/assets', express.static(STATICS_FOLDER)); 33 | app.use('/', express.static(STATICS_FOLDER)); 34 | app.use(bodyParser.json({ limit: config.MAX_DIFF_SIZE })); 35 | app.use(diffTooBigErrorHandler); 36 | 37 | function diffTooBigErrorHandler(err: any, req: any, res: any, next: any) { 38 | if (err.type == 'entity.too.large') { 39 | res.status(400).send({ error: 'The diff is to big, the limit is ' + config.MAX_DIFF_SIZE }) 40 | } else { 41 | next(err) 42 | } 43 | } 44 | 45 | app.use(cookieParser(config.session_secret)); // neded to read from req.cookie 46 | 47 | const metricsProvider = (gaCookie: string) => 48 | new GAMetrics(config.GA_ANALITYCS_KEY, gaCookie || config.GA_API_DEFAULT_KEY); 49 | let contextParserProvider = () => new ContextParser(); 50 | 51 | app.get('/api/diff/:id', toMPromise( 52 | () => new GetDiffInputFactory(), 53 | contextParserProvider, 54 | () => new GetSharedDiffAction(repo, metricsProvider))) 55 | app.put('/api/diff', toMPromise( 56 | () => new CreateDiffInputFactory(), 57 | contextParserProvider, 58 | () => new CreateSharedDiffAction(repo, metricsProvider))); 59 | app.delete('/api/diff/:id', toMPromise( 60 | () => new DeleteDiffInputFactory(), 61 | contextParserProvider, 62 | () => new DeleteSharedDiffAction(repo, metricsProvider))); 63 | app.post('/api/diff/makePermanent/:id', toMPromise( 64 | () => new MakeDiffPermanentInputFactory(), 65 | contextParserProvider, 66 | () => new MakePermanentSharedDiffAction(repo, metricsProvider))); 67 | app.post('/api/diff/extend/:id', toMPromise( 68 | () => new ExtendDiffLifetimeInputFactory(), 69 | contextParserProvider, 70 | () => new ExtendLifetimeSharedDiffAction(repo, metricsProvider))); 71 | 72 | app.get('/diff_download/:id', function (req: any, res: any) { 73 | var id = req.params.id; 74 | repo.fetchById(id) 75 | .then(diff => { 76 | if (diff === null) { 77 | res.status(404); 78 | res.send( 79 | '404 Sorry, the requested page was not found, create one at http://diffy.org'); 80 | return; 81 | } 82 | var rawDiff = diff.rawDiff; 83 | res.setHeader('Content-disposition', 'attachment; filename=' + id + '.diff'); 84 | res.setHeader('Content-type', 'text/plain'); 85 | res.send(rawDiff); 86 | }); 87 | }); 88 | app.get('*', function (req: any, res: any) { res.sendFile(INDEX_FILE); }); 89 | 90 | var server = app.listen(config.port, config.host, function () { 91 | var host = server.address().address; 92 | var port = server.address().port; 93 | 94 | console.log('App.ts listening at http://%s:%s', host, port); 95 | }); 96 | 97 | app.use(function (err: any, req: any, res: any, next: any) { 98 | console.error(err.stack); 99 | res.status(500).send('Something broke!'); 100 | }); 101 | 102 | // Make sure we exit gracefully when we receive a 103 | // SIGINT signal (eg. from Docker) 104 | process.on('SIGINT', function () { 105 | process.exit(); 106 | }); 107 | -------------------------------------------------------------------------------- /backend/src/SharedDiff.ts: -------------------------------------------------------------------------------- 1 | import * as Diff2Html from 'diff2html'; 2 | import { DiffFile } from 'diff2html/lib/types'; 3 | import { SharedDiff } from 'diffy-models'; 4 | 5 | const MAX_DIFF_DATE = new Date('9999-01-01'); 6 | const MILLIS_IN_A_DAY = 86400000.0; 7 | 8 | export function makeSharedDiff(raw_diff: string, createdDate: Date = new Date()): SharedDiff { 9 | const expireDate = calculateExpireDate(createdDate) 10 | return makeSharedDiffWithId(null, raw_diff, createdDate, expireDate) 11 | } 12 | 13 | /** 14 | * Returns a new SharedDiff with the expiresAt set at {@link MAX_DIFF_DATE} 15 | * @param diff - a shared diff 16 | * @returns - a new SharedDiff 17 | */ 18 | export function makePermanent(diff: SharedDiff): SharedDiff { 19 | return { ...diff, expiresAt: MAX_DIFF_DATE } 20 | } 21 | 22 | /** 23 | * Returns a new SharedDiff with a expiresAt date set to further in the future (by number of hours). 24 | * @param diff - a shared diff 25 | * @param hours - the number of hours by which to extend the expire time date 26 | * @returns - a new SharedDiff with the expiresAt date changed 27 | */ 28 | export function extendLifetime(diff: SharedDiff, hours: number): SharedDiff { 29 | const newDate = new Date(diff.expiresAt.getTime() + (hours * 60 * 60 * 1000)); 30 | return { ...diff, expiresAt: newDate } 31 | } 32 | 33 | export function makeSharedDiffWithId(id: string, raw_diff: string, createdDate: Date, expireDate: Date): SharedDiff { 34 | return { 35 | id: id, 36 | created: createdDate, 37 | expiresAt: expireDate, 38 | diff: Diff2Html.parse(raw_diff), 39 | rawDiff: raw_diff, 40 | }; 41 | } 42 | 43 | /** 44 | * Returns the number of time this diff has been extended. It uses a day (24 hours) as the unit 45 | * of extension and doesn't count the first 24 hours which are the default lifetime. 46 | */ 47 | export function lifetimeExtensionCount(diff: SharedDiff): number { 48 | const millis = diff.expiresAt.getTime() - diff.created.getTime() 49 | return Math.round((millis / (MILLIS_IN_A_DAY))) - 1; 50 | } 51 | 52 | function calculateExpireDate(date: Date) { 53 | let expire_date = new Date(); 54 | expire_date.setDate(date.getDate() + 1); 55 | return expire_date; 56 | } 57 | 58 | export function isValidRawDiff(raw_diff: string): boolean { 59 | const jsonDiff = Diff2Html.parse(raw_diff); 60 | if (_isObjectEmpty(jsonDiff)) { 61 | return false; 62 | } 63 | return true; 64 | } 65 | 66 | function _isObjectEmpty(obj: DiffFile[]): boolean { 67 | var name; 68 | for (name in obj) { 69 | return false; 70 | } 71 | return true; 72 | }; 73 | 74 | -------------------------------------------------------------------------------- /backend/src/actions/ActionUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | import { InputParser, Context, Input, Output, ActionPromise, ContextParser } from 'diffy-models'; 3 | 4 | export function toMPromise( 5 | inputProvider: () => InputParser, 6 | contextProvider: () => ContextParser, 7 | actionProvider: () => ActionPromise) { 8 | 9 | return function (req: any, res: any) { 10 | const i = inputProvider(); // TODO: no need to create a new instance every time 11 | const c = contextProvider(); // TODO: no need to create a new instance every time 12 | const a = actionProvider(); // TODO: no need to create a new instance every time 13 | let request: Input 14 | let context: Context 15 | try { 16 | request = i.parse(req) 17 | context = c.parse(req); 18 | } catch (error) { 19 | console.log("Error while parsing arguments: " + error); 20 | res.status(400) 21 | res.send("error while parsing the request"); 22 | return; 23 | } 24 | 25 | return a.execute(request, context).then(output => { 26 | res.status(200) 27 | res.json(output); 28 | }).catch(error => { 29 | console.log("Error while executing an action: " + error); 30 | res.status(500) 31 | res.send("oops, something went wrong ... "); 32 | }); 33 | } 34 | } -------------------------------------------------------------------------------- /backend/src/actions/CreateSharedDiffAction.ts: -------------------------------------------------------------------------------- 1 | import { Metrics } from '../metrics/Metrics'; 2 | import { isValidRawDiff, makeSharedDiff } from '../SharedDiff'; 3 | import { SharedDiff } from "diffy-models"; 4 | import { SharedDiffRepository } from '../sharedDiffRepository/SharedDiffRepository'; 5 | import { ActionPromise } from 'diffy-models'; 6 | import { Context } from 'diffy-models'; 7 | import { CreateDiffInput } from 'diffy-models'; 8 | import { GetDiffOutput } from 'diffy-models'; 9 | 10 | export class CreateSharedDiffAction extends ActionPromise { 11 | constructor( 12 | private repository: SharedDiffRepository, 13 | private metricsProvider: (gaCookie: string) => Metrics 14 | ) { 15 | super(); 16 | } 17 | 18 | public execute(input: CreateDiffInput, context: Context): Promise { 19 | const metrics = this.metricsProvider(context.gaCookie); 20 | if (!isValidRawDiff(input.diff)) { 21 | return Promise.reject("Diff is not valid"); 22 | } 23 | const sharedDiff = makeSharedDiff(input.diff); 24 | return this.storeSharedDiff(sharedDiff, metrics).then((obj: SharedDiff) => { 25 | if (!obj.id) { 26 | console.warn('new: undefined obj id'); 27 | return Promise.reject({ error: 'new: undefined obj id' }); 28 | } 29 | return Promise.resolve(new GetDiffOutput(obj)); 30 | }); 31 | } 32 | 33 | private storeSharedDiff(shared_diff: SharedDiff, metrics: Metrics): Promise { 34 | return this.repository.insert(shared_diff) 35 | .then( 36 | shared_diff => { 37 | metrics.diffStoredSuccessfully(); 38 | return shared_diff 39 | }, 40 | error => { 41 | metrics.diffFailedToStore(); 42 | return Promise.reject(error) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/src/actions/DeleteSharedDiffAction.ts: -------------------------------------------------------------------------------- 1 | import { DeleteDiffInput } from 'diffy-models'; 2 | import { DeleteDiffOutput } from 'diffy-models'; 3 | import { ActionPromise } from 'diffy-models'; 4 | import { Context } from 'diffy-models'; 5 | import { Metrics } from '../metrics/Metrics'; 6 | import { SharedDiffRepository } from '../sharedDiffRepository/SharedDiffRepository'; 7 | 8 | export class DeleteSharedDiffAction extends ActionPromise { 9 | 10 | constructor( 11 | private repository: SharedDiffRepository, 12 | private metricsProvider: (gaCookie: string) => Metrics 13 | ) { super() } 14 | 15 | public execute(input: DeleteDiffInput, context: Context): Promise { 16 | const metrics = this.metricsProvider(context.gaCookie); 17 | return this.repository.deleteById(input.id).then( 18 | deletedRows => { 19 | metrics.diffDeletedSuccessfully(); 20 | return new DeleteDiffOutput(true); 21 | }, 22 | error => { 23 | console.log(error); 24 | metrics.diffFailedToDelete(); 25 | return Promise.reject("There was an error when deleting the diff"); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/actions/ExtendLifetimeSharedDiffAction.ts: -------------------------------------------------------------------------------- 1 | import { Metrics } from '../metrics/Metrics'; 2 | import { extendLifetime, lifetimeExtensionCount } from '../SharedDiff'; 3 | import { ActionPromise, Context, ExtendDiffLifetimeInput, ExtendDiffLifetimeOutput, SharedDiff } from "diffy-models"; 4 | import { SharedDiffRepository } from '../sharedDiffRepository/SharedDiffRepository'; 5 | 6 | export class ExtendLifetimeSharedDiffAction extends ActionPromise { 7 | static readonly MAX_LIFETIME_OF_DIFF_MS = 5 * 24 * 60 * 60 * 1000; // 5 days 8 | 9 | constructor( 10 | private repository: SharedDiffRepository, 11 | private metricsProvider: (gaCookie: string) => Metrics 12 | ) { super(); } 13 | 14 | execute(input: ExtendDiffLifetimeInput, context: Context): Promise { 15 | const numberOfHours = 24; 16 | const metrics = this.metricsProvider(context.gaCookie); 17 | return this.repository.fetchById(input.id) 18 | .then(diff => { 19 | let newDate: Date = new Date(diff.expiresAt.getTime() + (numberOfHours * 60 * 60 * 1000)); 20 | if (newDate.getTime() - diff.created.getTime() > 21 | ExtendLifetimeSharedDiffAction.MAX_LIFETIME_OF_DIFF_MS) { 22 | return Promise.reject({ 23 | success: false, 24 | message: 'Can\'t extend beyond the maximum lifetime of a diff which is 5 days.' + 25 | ' If this is needed, please fill a new issue on Github with the use case.' 26 | }); 27 | } 28 | return extendLifetime(diff, numberOfHours); 29 | }) 30 | .then(diff => this.repository.update(diff)) 31 | .then(diff => { 32 | const extensionsCount = lifetimeExtensionCount(diff); 33 | metrics.diffLifetimeExtendedSuccessfully(extensionsCount); 34 | return new ExtendDiffLifetimeOutput(diff); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/actions/GetSharedDiffAction.ts: -------------------------------------------------------------------------------- 1 | import { SharedDiffRepository } from '../sharedDiffRepository/SharedDiffRepository'; 2 | import { ActionPromise, Context, GetDiffInput, GetDiffOutput } from 'diffy-models'; 3 | import { Metrics } from '../metrics/Metrics'; 4 | 5 | export class GetSharedDiffAction extends ActionPromise { 6 | constructor( 7 | private repository: SharedDiffRepository, 8 | private metricsProvider: (gaCookie: string) => Metrics 9 | ) { super(); } 10 | 11 | execute(input: GetDiffInput, context: Context): Promise { 12 | const metrics = this.metricsProvider(context.gaCookie); 13 | return this.repository.fetchById(input.id).then(result => { 14 | metrics.diffRetrievedSuccessfully(); 15 | return new GetDiffOutput(result); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/actions/MakePermanentSharedDiffAction.ts: -------------------------------------------------------------------------------- 1 | import { Metrics } from '../metrics/Metrics'; 2 | import { makePermanent } from '../SharedDiff'; 3 | import { ActionPromise, Context, MakeDiffPermanentInput, MakeDiffPermanentOutput, SharedDiff } from "diffy-models"; 4 | import { SharedDiffRepository } from '../sharedDiffRepository/SharedDiffRepository'; 5 | 6 | export class MakePermanentSharedDiffAction extends ActionPromise { 7 | static readonly MAX_LIFETIME_OF_DIFF_MS = 5 * 24 * 60 * 60 * 1000; // 5 days 8 | 9 | constructor( 10 | private repository: SharedDiffRepository, 11 | private metricsProvider: (gaCookie: string) => Metrics 12 | ) { super(); } 13 | 14 | execute(input: MakeDiffPermanentInput, context: Context): Promise { 15 | const metrics = this.metricsProvider(context.gaCookie); 16 | return this.repository.fetchById(input.id) 17 | .then(diff => makePermanent(diff)) 18 | .then(diff => this.repository.update(diff)) 19 | .then(result => { 20 | metrics.diffMadePermanentSuccesfully(); 21 | return new MakeDiffPermanentOutput(result); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/config.js: -------------------------------------------------------------------------------- 1 | var config = {}; 2 | 3 | config.host = process.env.DIFFY_WEB_HOST || '127.0.0.1'; 4 | config.port = parseInt(process.env.DIFFY_WEB_PORT) || 3000; 5 | 6 | config.session_collection = 'sessions'; 7 | config.session_secret = process.env.DIFFY_SESSION_SECRET || 'not-that-secret'; 8 | 9 | config.GA_ANALITYCS_KEY = process.env.DIFFY_GA_ANALYTICS_KEY; 10 | config.GA_DIFFY_API_KEY = "diffApi"; 11 | config.GA_API_DEFAULT_KEY = ""; 12 | 13 | config.MAX_DIFF_SIZE = '1mb'; 14 | config.DIFF_REPO = { 15 | type: "double_write", 16 | primary: { 17 | type: "mongo", 18 | db_host: process.env.DIFFY_DB_HOST || '127.0.0.1', 19 | db_port: process.env.DIFFY_DB_PORT || '27017', 20 | db_name: 'diffy', 21 | }, 22 | secondary: { 23 | type: "google" 24 | } 25 | }; 26 | 27 | module.exports = config; 28 | -------------------------------------------------------------------------------- /backend/src/config_dev.js: -------------------------------------------------------------------------------- 1 | var config = {}; 2 | 3 | config.host = process.env.DIFFY_WEB_HOST || '127.0.0.1'; 4 | config.port = parseInt(process.env.DIFFY_WEB_PORT) || 3000; 5 | 6 | config.session_collection = 'sessions'; 7 | config.session_secret = process.env.DIFFY_SESSION_SECRET || 'not-that-secret'; 8 | 9 | config.GA_ANALITYCS_KEY = process.env.DIFFY_GA_ANALYTICS_KEY; 10 | config.GA_DIFFY_API_KEY = "diffApi"; 11 | config.GA_API_DEFAULT_KEY = ""; 12 | 13 | config.MAX_DIFF_SIZE = '1mb'; 14 | config.DIFF_REPO = { 15 | type: "memory", 16 | }; 17 | 18 | module.exports = config; 19 | -------------------------------------------------------------------------------- /backend/src/crons/delete_expired_diffs_cron.ts: -------------------------------------------------------------------------------- 1 | import { getRepositorySupplierFor } from "../sharedDiffRepository/SharedDiffRepository"; 2 | //var config = require('../config'); 3 | let config = { 4 | DIFF_REPO: { 5 | type: "google" 6 | } 7 | } 8 | 9 | function main() { 10 | const repo = getRepositorySupplierFor(config.DIFF_REPO)(); 11 | repo.deleteExpired(); 12 | } 13 | 14 | main(); 15 | -------------------------------------------------------------------------------- /backend/src/metrics/GAMetrics.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | 3 | import {Metrics} from './Metrics'; 4 | 5 | /** 6 | * This class implements the protocol for communicating with GA described here: 7 | * https://developers.google.com/analytics/devguides/collection/protocol/v1/ 8 | */ 9 | export class GAMetrics implements Metrics { 10 | key: string; 11 | options: any; 12 | clientId: string; 13 | constructor(key: string, clientId: string) { 14 | this.key = key; 15 | this.clientId = clientId; 16 | 17 | this.options = { 18 | hostname: 'www.google-analytics.com', 19 | path: '/collect', 20 | method: 'POST', 21 | }; 22 | } 23 | private logStr(level: string, str: string) { 24 | if (level === 'info') { 25 | console.info('LogBasedMetrics: ' + str); 26 | } else if (level === 'error') { 27 | console.error('LogBasedMetrics: ' + str); 28 | } 29 | } 30 | private sendRequest(data: string) { 31 | const req = http.request(this.options, (res) => {}); 32 | req.on('error', (e) => { 33 | this.logStr('error', `problem with GA request: ${e.message}`); 34 | }); 35 | // write data to request body 36 | req.write(data); 37 | req.end(); 38 | } 39 | diffStoredSuccessfully() { 40 | const data = 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=created&tid=' + this.key; 41 | this.sendRequest(data); 42 | } 43 | diffFailedToStore() { 44 | const data = 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=created_failed&tid=' + this.key; 45 | this.sendRequest(data); 46 | } 47 | diffStoredSuccessfullyFromAPI() { 48 | const data = 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=created_api&tid=' + this.key; 49 | this.sendRequest(data); 50 | } 51 | diffFailedToStoreFromAPI() { 52 | const data = 53 | 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=created_failed_api&tid=' + this.key; 54 | this.sendRequest(data); 55 | } 56 | diffDeletedSuccessfully() { 57 | const data = 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=deleted&tid=' + this.key; 58 | this.sendRequest(data); 59 | } 60 | diffFailedToDelete() { 61 | const data = 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=deleted_failed&tid=' + this.key; 62 | this.sendRequest(data); 63 | } 64 | diffRetrievedSuccessfully() { 65 | const data = 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=diff_retrieved&tid=' + this.key; 66 | this.sendRequest(data); 67 | } 68 | diffLifetimeExtendedSuccessfully(n: number) { 69 | const data = 70 | 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=diff_ttl_extended&tid=' + this.key + '&el=' + n; 71 | this.sendRequest(data); 72 | } 73 | 74 | diffMadePermanentSuccesfully() { 75 | const data = 76 | 'v=1&cid=' + this.clientId + '&t=event&ec=diff&ea=diff_made_permanent&tid=' + this.key; 77 | this.sendRequest(data); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /backend/src/metrics/LogBasedMetrics.ts: -------------------------------------------------------------------------------- 1 | import {Metrics} from './Metrics'; 2 | 3 | export class LogBasedMetrics implements Metrics { 4 | constructor() {} 5 | private logStr(level: string, str: string) { 6 | if (level === 'info') { 7 | console.info('LogBasedMetrics: ' + str); 8 | } else if (level === 'error') { 9 | console.error('LogBasedMetrics: ' + str); 10 | } 11 | } 12 | diffStoredSuccessfully() { 13 | this.logStr('info', 'Diff stored successfully'); 14 | } 15 | diffFailedToStore() { 16 | this.logStr('error', 'Diff failed to store'); 17 | } 18 | diffStoredSuccessfullyFromAPI() { 19 | this.logStr('info', 'API: Diff stored successfully'); 20 | } 21 | diffFailedToStoreFromAPI() { 22 | this.logStr('error', 'API: Diff failed to store'); 23 | } 24 | diffDeletedSuccessfully() { 25 | this.logStr('info', 'Diff deleted successfully'); 26 | } 27 | diffFailedToDelete() { 28 | this.logStr('error', 'Diff failed to delete'); 29 | } 30 | diffRetrievedSuccessfully() { 31 | this.logStr('info', 'Diff retrieved successfully'); 32 | } 33 | diffLifetimeExtendedSuccessfully(n: number) { 34 | this.logStr('info', `Diff lifetime extended successfully ${n} times`); 35 | } 36 | 37 | diffMadePermanentSuccesfully() { 38 | this.logStr('info', 'Diff made permanent successfully'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/metrics/Metrics.ts: -------------------------------------------------------------------------------- 1 | export interface Metrics { 2 | diffStoredSuccessfully: () => void; 3 | diffFailedToStore: () => void; 4 | diffStoredSuccessfullyFromAPI: () => void; 5 | diffFailedToStoreFromAPI: () => void; 6 | diffDeletedSuccessfully: () => void; 7 | diffFailedToDelete: () => void; 8 | diffRetrievedSuccessfully: () => void; 9 | /** 10 | * @param n: number of times the diff has been extended 11 | */ 12 | diffLifetimeExtendedSuccessfully: (n: number) => void; 13 | diffMadePermanentSuccesfully: () => void; 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/scripts/add_created_and_expired_dates_to_rows.js: -------------------------------------------------------------------------------- 1 | var MongoClient = require('mongodb').MongoClient; 2 | var config = require('../config'); 3 | var url = config.db_url; // legacy, this config is discontinued 4 | 5 | function main() { 6 | var now = new Date; 7 | MongoClient.connect(url, function(err, db) { 8 | var diffy = db.collection('diffy'); 9 | var cursor = diffy.find({}, { _id: 1, created: 1, expiresAt: 1 }).sort({ created: 1 }); 10 | cursor.each(function(err, doc) { 11 | if (doc == null) { 12 | db.close(); 13 | return; 14 | } 15 | if (!doc.created) { 16 | console.log("not created: " + doc._id); 17 | diffy.update({ _id: doc._id }, { '$set': { created: new Date } }); 18 | } 19 | if (!doc.expiresAt) { 20 | console.log("not expiresAt: " + doc._id); 21 | diffy.update({ _id: doc._id }, { '$set': { expiresAt: new Date } }); 22 | } 23 | }); 24 | }); 25 | } 26 | 27 | main(); 28 | -------------------------------------------------------------------------------- /backend/src/scripts/diff_copy_tool.ts: -------------------------------------------------------------------------------- 1 | import mongodb = require('mongodb'); 2 | import { buildDbUrl, MongoSharedDiffRepository } from "../sharedDiffRepository/MongoSharedDiffRepository"; 3 | import { GoogleDatastoreDiffRepository } from "../sharedDiffRepository/GoogleDatastoreDiffRepository"; 4 | import { Datastore } from "@google-cloud/datastore"; 5 | import * as readline from 'readline'; 6 | 7 | const db_host = '127.0.0.1'; 8 | const db_port = '27017'; 9 | const db_url = buildDbUrl(db_host, db_port); 10 | const collection = mongodb.MongoClient.connect(db_url) 11 | .then(client => client.db(db_name)) 12 | .then(db => db.collection(MongoSharedDiffRepository.COLLECTION_NAME)); 13 | 14 | const db_name = "diffy"; 15 | const ds = new Datastore(); 16 | 17 | const srcRepo = new MongoSharedDiffRepository(collection); 18 | const dstRepo = new GoogleDatastoreDiffRepository(ds); 19 | 20 | const rl = readline.createInterface({ 21 | input: process.stdin, 22 | output: process.stdout, 23 | terminal: false 24 | }) 25 | 26 | rl.on('line', function(line){ 27 | srcRepo.fetchById(line) 28 | .then(diff => dstRepo.update(diff)) 29 | .then(diff => console.info(`Successfully copied ${diff.id}`)) 30 | .catch(e => console.warn(`Failed to copy ${line} with error`, e)) 31 | }) -------------------------------------------------------------------------------- /backend/src/sharedDiffRepository/DoubleWriteDiffRepository.ts: -------------------------------------------------------------------------------- 1 | import { SharedDiff } from "diffy-models"; 2 | import { SharedDiffRepository } from "./SharedDiffRepository"; 3 | 4 | export class DoubleWriteDiffRepository implements SharedDiffRepository { 5 | private masterRepo: SharedDiffRepository; 6 | private followerRepo: SharedDiffRepository; 7 | 8 | constructor(masterRepo: SharedDiffRepository, followerRepo: SharedDiffRepository) { 9 | this.masterRepo = masterRepo; 10 | this.followerRepo = followerRepo; 11 | } 12 | 13 | insert(diff: SharedDiff): Promise { 14 | const masterResult = this.masterRepo.insert(diff) 15 | masterResult.then(diff => { 16 | this.followerRepo.update(diff) 17 | .catch(err => console.trace( 18 | `Failed to double insert diff with id ${diff.id}`, 19 | JSON.stringify(err, null, ' '))) 20 | }); 21 | return masterResult; 22 | } 23 | 24 | fetchById(id: string): Promise { 25 | return this.masterRepo.fetchById(id); 26 | } 27 | 28 | deleteById(id: string): Promise { 29 | const masterResult = this.masterRepo.deleteById(id); 30 | masterResult.then(_ => { 31 | this.followerRepo.deleteById(id) 32 | .catch(err => console.trace( 33 | `Failed to delete update diff with id ${id}`, 34 | JSON.stringify(err, null, ' '))) 35 | }); 36 | return masterResult; 37 | } 38 | 39 | update(diff: SharedDiff): Promise { 40 | const masterResult = this.masterRepo.update(diff); 41 | masterResult.then(diff => { 42 | this.followerRepo.update(diff) 43 | .catch(err => console.trace( 44 | `Failed to double update diff with id ${diff.id}`, 45 | JSON.stringify(err, null, ' '))) 46 | }); 47 | return masterResult; 48 | } 49 | 50 | deleteExpired(): Promise { 51 | const masterResult = this.masterRepo.deleteExpired(); 52 | masterResult.then(primaryResult => { 53 | this.followerRepo.deleteExpired() 54 | .catch(err => console.trace( 55 | `Failed to double delete expired diff`, 56 | JSON.stringify(err, null, ' '))) 57 | }); 58 | return masterResult; 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /backend/src/sharedDiffRepository/GoogleDatastoreDiffRepository.ts: -------------------------------------------------------------------------------- 1 | // Imports the Google Cloud client library 2 | import { Datastore } from '@google-cloud/datastore'; 3 | 4 | import { SharedDiff } from "diffy-models"; 5 | import { makeSharedDiffWithId } from '../SharedDiff'; 6 | import { SharedDiffRepository } from './SharedDiffRepository'; 7 | const utils = require('../utils.js').Utils; 8 | 9 | const ENTITY_NAME = 'diffy'; // maybe should be SharedDiff 10 | 11 | export class GoogleDatastoreDiffRepository implements SharedDiffRepository { 12 | datastore: Datastore; 13 | 14 | constructor(datastore: Datastore) { 15 | this.datastore = datastore; 16 | } 17 | 18 | insert(diff: SharedDiff): Promise { 19 | const url_id = utils.genRandomString() 20 | const row = { 21 | url_id: url_id, 22 | rawDiff: diff.rawDiff, 23 | created: diff.created, 24 | expiresAt: diff.expiresAt 25 | } 26 | return this.datastore.save({ 27 | key: this.datastore.key([ENTITY_NAME, url_id]), 28 | data: row, 29 | excludeFromIndexes: [ 30 | 'rawDiff', // important, otherwise google fails saying that is longer than 1500 bytes 31 | ], 32 | }).then(_ => ({ ...diff, id: url_id })); 33 | } 34 | 35 | /** 36 | * This method is more of an upsert really, it will update or inster if it doesn't exist. 37 | * @param diff - data to be updated 38 | * @returns 39 | */ 40 | update(diff: SharedDiff): Promise { 41 | const row = { 42 | url_id: diff.id, 43 | rawDiff: diff.rawDiff, 44 | created: diff.created, 45 | expiresAt: diff.expiresAt 46 | } 47 | return this.datastore.save({ 48 | key: this.datastore.key([ENTITY_NAME, diff.id]), 49 | data: row, 50 | excludeFromIndexes: [ 51 | 'rawDiff', // important, otherwise google fails saying that is longer than 1500 bytes 52 | ], 53 | }).then(_ => (diff)); 54 | } 55 | 56 | fetchById(id: string): Promise { 57 | const query = this.datastore.createQuery(ENTITY_NAME) 58 | .filter("url_id", "=", id) 59 | .limit(1); 60 | return this.datastore.runQuery(query) 61 | .then(diffys => diffys[0][0]) 62 | .then(diffy => makeSharedDiffWithId(diffy.url_id, diffy.rawDiff, diffy.created, diffy.expiresAt)); 63 | } 64 | 65 | // returns a promise of how many items where 66 | // deleted 67 | deleteById(id: string): Promise { 68 | // can't seem to figure out how get the number of deletions from google datastore 69 | return this.datastore.delete(this.datastore.key(["diffy", id])) 70 | .then(() => 1); 71 | } 72 | 73 | deleteExpired(): Promise { 74 | const query = this.datastore.createQuery(ENTITY_NAME) 75 | .filter("expiresAt", "<=", new Date()) 76 | return this.datastore.runQuery(query) 77 | .then(diffys => diffys[0]) 78 | .then(expiredDiffys => { 79 | return this.datastore.delete(expiredDiffys.map(d => d[this.datastore.KEY])); 80 | }).then(r => true); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /backend/src/sharedDiffRepository/MemoryDiffRepository.ts: -------------------------------------------------------------------------------- 1 | import { SharedDiff } from "diffy-models"; 2 | import { SharedDiffRepository } from "./SharedDiffRepository"; 3 | const utils = require('../utils.js').Utils; 4 | 5 | export class MemoryDiffRepository implements SharedDiffRepository { 6 | private db: { [id: string]: SharedDiff } = {}; 7 | 8 | constructor() { 9 | console.warn("Using a memory diff repositoy. This is not suitable for production"); 10 | } 11 | 12 | insert(diff: SharedDiff): Promise { 13 | const id = utils.genRandomString() 14 | this.db[id] = { ...diff, id }; 15 | return Promise.resolve(this.db[id]); 16 | } 17 | 18 | fetchById(id: string): Promise { 19 | const diff = this.db[id]; 20 | if (diff == null) { 21 | return Promise.reject("not found"); 22 | } 23 | return Promise.resolve({ ...diff }); 24 | } 25 | 26 | deleteById(id: string): Promise { 27 | delete this.db[id]; 28 | return Promise.resolve(1); 29 | } 30 | 31 | update(diff: SharedDiff): Promise { 32 | if (diff.id == null) { 33 | return Promise.reject("diff was mising an id"); 34 | } 35 | this.db[diff.id] = { ...diff }; 36 | return Promise.resolve(this.db[diff.id]); 37 | } 38 | 39 | deleteExpired(): Promise { 40 | throw new Error("Method not implemented."); 41 | } 42 | } -------------------------------------------------------------------------------- /backend/src/sharedDiffRepository/MongoSharedDiffRepository.ts: -------------------------------------------------------------------------------- 1 | import mongodb = require('mongodb'); 2 | import { SharedDiff } from "diffy-models"; 3 | import { makeSharedDiffWithId } from '../SharedDiff'; 4 | import { SharedDiffRepository} from './SharedDiffRepository'; 5 | const utils = require('../utils.js').Utils; 6 | 7 | export function buildDbUrl(host:string , port: string): string { 8 | return "mongodb://" + host + ":" + port + "/diffy"; 9 | } 10 | 11 | export class MongoSharedDiffRepository implements SharedDiffRepository { 12 | 13 | static COLLECTION_NAME: string = 'diffy'; 14 | 15 | collection: Promise 16 | 17 | constructor(collection: Promise) { 18 | this.collection = collection; 19 | } 20 | 21 | insert(diff: SharedDiff): Promise { 22 | return this.collection 23 | .then(collection => collection.insertOne({...diff, _id: utils.genRandomString()})) 24 | .then(result => result.insertedId.toString()) 25 | .then(id => ({...diff, id})); 26 | } 27 | 28 | update(diff: SharedDiff): Promise { 29 | return this.collection 30 | .then(collection => collection.replaceOne({_id: diff.id}, { 31 | rawDiff: diff.rawDiff, 32 | expiresAt: diff.expiresAt, 33 | created: diff.created 34 | })) 35 | .then(result => ({ ...diff })); 36 | } 37 | 38 | fetchById(id: string): Promise { 39 | return this.collection 40 | .then(collection => collection.findOne({'_id': id})) 41 | .then(doc => makeSharedDiffWithId(doc._id, doc.rawDiff, doc.created, doc.expiresAt)); 42 | } 43 | 44 | // returns a promise of how many items where 45 | // deleted 46 | deleteById(id: string): Promise { 47 | return this.collection 48 | .then(collection => collection.deleteOne({'_id': id})) 49 | .then(result => result.deletedCount); 50 | } 51 | 52 | deleteExpired(): Promise { 53 | return this.collection 54 | .then(collection => collection.deleteMany({ expiresAt: { '$lte': new Date } })) 55 | .then(result => { return true; /* success, TODO: add metrics in the future */ }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /backend/src/sharedDiffRepository/SharedDiffRepository.ts: -------------------------------------------------------------------------------- 1 | import mongodb = require('mongodb'); 2 | import { Datastore } from '@google-cloud/datastore'; 3 | import { SharedDiff } from "diffy-models"; 4 | import { DoubleWriteDiffRepository } from './DoubleWriteDiffRepository'; 5 | import { GoogleDatastoreDiffRepository } from './GoogleDatastoreDiffRepository'; 6 | import { buildDbUrl, MongoSharedDiffRepository } from './MongoSharedDiffRepository'; 7 | import { MemoryDiffRepository } from './MemoryDiffRepository'; 8 | 9 | export interface SharedDiffRepository { 10 | insert: (diff: SharedDiff) => Promise; 11 | fetchById: (id: string) => Promise; 12 | deleteById: (id: string) => Promise; 13 | update(diff: SharedDiff): Promise; 14 | deleteExpired(): Promise 15 | } 16 | 17 | /** 18 | * Returns a function that when invoked, will build a repository 19 | * @param config - a DIFF_REPO config object used to initialize the App 20 | * @returns a non-parametrized function to build a repository of said type 21 | */ 22 | export function getRepositorySupplierFor(config: any): () => SharedDiffRepository { 23 | if (config["type"] == "mongo") { 24 | return () => { 25 | const db_url = buildDbUrl(config["db_host"], config["db_port"]); 26 | const db_name = config["db_name"]; 27 | const collection = mongodb.MongoClient.connect(db_url) 28 | .then(client => client.db(db_name)) 29 | .then(db => db.collection(MongoSharedDiffRepository.COLLECTION_NAME)) 30 | 31 | return new MongoSharedDiffRepository(collection); 32 | } 33 | 34 | } else if (config["type"] == "google") { 35 | return () => new GoogleDatastoreDiffRepository(new Datastore()) 36 | 37 | } else if (config["type"] == "memory") { 38 | return () => new MemoryDiffRepository(); 39 | 40 | } else if (config["type"] == "double_write") { 41 | return () => new DoubleWriteDiffRepository( 42 | getRepositorySupplierFor(config["primary"])(), 43 | getRepositorySupplierFor(config["secondary"])(), 44 | ); 45 | } 46 | throw "unknown diff repo"; 47 | } -------------------------------------------------------------------------------- /backend/src/utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | function Utils() { 4 | } 5 | 6 | Utils.prototype.getFileName = function(file) { 7 | return file.newName == '/dev/null' ? file.oldName : file.newName; 8 | }; 9 | 10 | Utils.prototype.sortByFilenameCriteria = function(file1, file2) { 11 | // instantiating here because this can be used as 12 | // a callback and the meaning of this would be lost 13 | var utils = new Utils(); 14 | var fileName1 = utils.getFileName(file1); 15 | var fileName2 = utils.getFileName(file2); 16 | if (fileName1 > fileName2) return 1; 17 | if (fileName1 < fileName2) return -1; 18 | return 0; 19 | }; 20 | 21 | Utils.prototype.genRandomString = function() { 22 | // A random decimal in the range [0, 1) converted to a base 16 (hexadecimal) string 23 | // with the first two chars (0.) removed 24 | return (Math.random()).toString(16).substring(2); 25 | }; 26 | 27 | Utils.prototype.isObjectEmpty = function(obj) { 28 | var name; 29 | for (name in obj) { 30 | return false; 31 | } 32 | return true; 33 | }; 34 | 35 | Utils.prototype.exceedsFileSizeLimit = function(multerFile) { 36 | // must be less than a megabyte 37 | return multerFile.size > 1000000; 38 | }; 39 | 40 | Utils.prototype.createDiffObject = function(diff, jsonDiff) { 41 | var id = Utils.prototype.genRandomString(); 42 | // create object 43 | var created = new Date(); 44 | var expiresAt = new Date(); 45 | expiresAt.setDate(created.getDate() + 1); 46 | var obj = { 47 | _id: id, 48 | diff: jsonDiff, 49 | rawDiff: diff, 50 | created: created, 51 | expiresAt: expiresAt, 52 | }; 53 | return obj; 54 | }; 55 | 56 | // expose this module 57 | module.exports.Utils = new Utils(); 58 | 59 | })(); 60 | 61 | -------------------------------------------------------------------------------- /backend/src/utils/ConfigFileResolver.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The purpose of this class is to wrap the logic to resolve a config file. 3 | * The steps to resolve work as follow: 4 | * 1. First, it will try to read the first argument passed to the program. 5 | * 2. If it doesn't find anything, it will read from the environment variable 6 | * DIFFY_CONFIG_FILE. 7 | * 3. If none of the above resolves a file name, it will read NODE_ENV from the environment 8 | * and will use './config' if on production, './confid_dev' otherwise. 9 | */ 10 | export class ConfigFileResolver { 11 | static resolve(argv: string[], env: NodeJS.ProcessEnv) { 12 | var config_file = argv[2]; 13 | if (config_file) { 14 | console.info(`Using config file provided: ${config_file}`) 15 | } else { 16 | if (env["NODE_ENV"] == "production") { 17 | config_file = './config'; 18 | } else { 19 | config_file = './config_dev'; 20 | } 21 | } 22 | return config_file; 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /backend/tests/MockedMetrics.ts: -------------------------------------------------------------------------------- 1 | import {Metrics} from '../src/metrics/Metrics'; 2 | 3 | export const metrics: Metrics = { 4 | diffStoredSuccessfully: jest.fn(), 5 | diffFailedToStore: jest.fn(), 6 | diffStoredSuccessfullyFromAPI: jest.fn(), 7 | diffFailedToStoreFromAPI: jest.fn(), 8 | diffDeletedSuccessfully: jest.fn(), 9 | diffFailedToDelete: jest.fn(), 10 | diffRetrievedSuccessfully: jest.fn(), 11 | diffLifetimeExtendedSuccessfully: jest.fn(), 12 | diffMadePermanentSuccesfully: jest.fn(), 13 | }; 14 | -------------------------------------------------------------------------------- /backend/tests/SharedDiff.test.ts: -------------------------------------------------------------------------------- 1 | import {extendLifetime, isValidRawDiff, lifetimeExtensionCount, makeSharedDiff} from '../src/SharedDiff'; 2 | 3 | test('should create shared diff', () => { 4 | const raw_diff = ` 5 | diff --git a/file.json b/file.json 6 | index 1456e89..e1da2da 100644 7 | --- a/file.json 8 | +++ b/file.json 9 | @@ -1,1 +1,1 @@ 10 | -a 11 | +b 12 | ` 13 | const date = new Date(); 14 | let expire_date = new Date(); 15 | expire_date.setDate(date.getDate() + 1); 16 | 17 | const shared_diff = makeSharedDiff(raw_diff, date); 18 | expect(shared_diff.created).toEqual(date); 19 | expect(shared_diff.expiresAt).toEqual(expire_date); 20 | expect(shared_diff.diff[0].newName).toEqual('file.json'); 21 | expect(shared_diff.diff[0].oldName).toEqual('file.json'); 22 | expect(shared_diff.rawDiff).toEqual(raw_diff); 23 | }); 24 | 25 | test('should create shared diff with defaults', () => { 26 | const raw_diff = ` 27 | diff --git a/file.json b/file.json 28 | index 1456e89..e1da2da 100644 29 | --- a/file.json 30 | +++ b/file.json 31 | @@ -1,1 +1,1 @@ 32 | -a 33 | +b 34 | ` 35 | const shared_diff = makeSharedDiff(raw_diff); 36 | }); 37 | 38 | test('isValidRawDiff(): should validate a (valid) raw diff', () => { 39 | const raw_diff = ` 40 | diff --git a/file.json b/file.json 41 | index 1456e89..e1da2da 100644 42 | --- a/file.json 43 | +++ b/file.json 44 | @@ -1,1 +1,1 @@ 45 | -a 46 | +b 47 | ` 48 | expect(isValidRawDiff(raw_diff)).toBe(true); 49 | }); 50 | 51 | test('isValidRawDiff(): should fail validation when (invalid) raw diff', () => { 52 | const raw_diff = ` 53 | -a 54 | +b 55 | ` 56 | expect(isValidRawDiff(raw_diff)).toBe(false); 57 | }); 58 | 59 | test('lifetimeExtensionsCount()', () => { 60 | const raw_diff = ` 61 | diff --git a/file.json b/file.json 62 | index 1456e89..e1da2da 100644 63 | --- a/file.json 64 | +++ b/file.json 65 | @@ -1,1 +1,1 @@ 66 | -a 67 | +b 68 | ` 69 | let sharedDiff = makeSharedDiff(raw_diff); 70 | expect(lifetimeExtensionCount(sharedDiff)).toEqual(0); 71 | 72 | // Extend it for a year a day at a time and test 73 | for(let i = 1; i <= 366; i++) { 74 | sharedDiff = extendLifetime(sharedDiff, 24) 75 | expect(lifetimeExtensionCount(sharedDiff)).toEqual(i); 76 | } 77 | }); -------------------------------------------------------------------------------- /backend/tests/actions/ActionUtils.test.ts: -------------------------------------------------------------------------------- 1 | import { toMPromise } from "../../src/actions/ActionUtils"; 2 | import { MemoryDiffRepository } from "../../src/sharedDiffRepository/MemoryDiffRepository" 3 | import { makeSharedDiff } from "../../src/SharedDiff"; 4 | import { ContextParser, GetDiffInputFactory, Output } from "diffy-models"; 5 | import { GetSharedDiffAction } from "../../src/actions/GetSharedDiffAction"; 6 | import { metrics } from '../MockedMetrics'; 7 | 8 | test("toM promise", () => { 9 | const rawDiff = ` 10 | diff --git a/file.json b/file.json 11 | index 1456e89..e1da2da 100644 12 | --- a/file.json 13 | +++ b/file.json 14 | @@ -1,1 +1,1 @@ 15 | -a 16 | +b 17 | ` 18 | const sharedDiff = makeSharedDiff(rawDiff); 19 | const repo = new MemoryDiffRepository(); 20 | return repo.insert(sharedDiff).then(storedDiff => { 21 | expect(storedDiff.id).not.toBeNull() 22 | let req = { 23 | params: { id: storedDiff.id } 24 | }; 25 | let res = { 26 | status: jest.fn(statusCode => { }), 27 | json: jest.fn(output => { }), 28 | send: jest.fn(output => { }), 29 | } 30 | return toMPromise(() => new GetDiffInputFactory(), () => new ContextParser(), () => new GetSharedDiffAction(repo, () => metrics))(req, res) 31 | .then(() => { 32 | expect(res.status.mock.calls.length).toBe(1); 33 | expect(res.json.mock.calls.length).toBe(1); 34 | expect(res.json.mock.calls[0][0].sharedDiff.id).toBe(storedDiff.id) 35 | expect(res.json.mock.calls[0][0].sharedDiff.rawDiff).toBe(rawDiff) 36 | }); 37 | }); 38 | }); -------------------------------------------------------------------------------- /backend/tests/actions/CreateSharedDiffAction.test.ts: -------------------------------------------------------------------------------- 1 | import { CreateSharedDiffAction } from '../../src/actions/CreateSharedDiffAction'; 2 | import { Context, SharedDiff } from 'diffy-models'; 3 | import { SharedDiffRepository } from '../../src/sharedDiffRepository/SharedDiffRepository'; 4 | 5 | import { metrics } from '../MockedMetrics'; 6 | 7 | test('should create a CreateSharedDiffAction, create the SharedDiff and store it', () => { 8 | const raw_diff = ` 9 | diff --git a/file.json b/file.json 10 | index 1456e89..e1da2da 100644 11 | --- a/file.json 12 | +++ b/file.json 13 | @@ -1,1 +1,1 @@ 14 | -a 15 | +b 16 | ` 17 | const repo: SharedDiffRepository = { 18 | insert: jest.fn(diff => Promise.resolve({ ... diff, id:"abcd" })), 19 | fetchById: (id: string) => null, 20 | deleteById: (id: string) => Promise.resolve(0), 21 | update: (diff: SharedDiff) => Promise.reject('random err'), 22 | deleteExpired: jest.fn(), 23 | }; 24 | const action = new CreateSharedDiffAction(repo, () => metrics); 25 | return action.execute({ diff: raw_diff }, {} as Context) 26 | .then(output => { 27 | expect(output.sharedDiff.diff).toBeDefined(); 28 | expect(repo.insert).toHaveBeenCalled(); 29 | expect(metrics.diffStoredSuccessfully).toHaveBeenCalled(); 30 | }) 31 | }); 32 | 33 | test('CreateSharedDiffAction.storeSharedDiff(), store fails when inserting', () => { 34 | const raw_diff = ` 35 | diff --git a/file.json b/file.json 36 | index 1456e89..e1da2da 100644 37 | --- a/file.json 38 | +++ b/file.json 39 | @@ -1,1 +1,1 @@ 40 | -a 41 | +b 42 | ` 43 | const repo: SharedDiffRepository = { 44 | // insert: (diff: SharedDiff) => ({ id: 45 | // '1', ...diff }), 46 | insert: jest.fn((diff) => Promise.reject('fake error')), 47 | fetchById: (id: string) => null, 48 | deleteById: (id: string) => Promise.resolve(0), 49 | update: (diff: SharedDiff) => Promise.reject('random err'), 50 | deleteExpired: jest.fn(), 51 | }; 52 | const action = new CreateSharedDiffAction(repo, () => metrics); 53 | return action.execute({ diff: raw_diff }, {} as Context) 54 | .then(() => fail('should never reach')) 55 | .catch(() => { 56 | expect(repo.insert).toHaveBeenCalled(); 57 | expect(metrics.diffFailedToStore).toHaveBeenCalled(); 58 | }); 59 | }); 60 | 61 | test('CreateSharedDiffAction.storeSharedDiff(), rejects when invalid diff', () => { 62 | const raw_diff = ''; 63 | const repo: SharedDiffRepository = { 64 | insert: jest.fn().mockReturnValueOnce(new Promise(() => { })), 65 | fetchById: (id: string) => null, 66 | deleteById: (id: string) => Promise.resolve(0), 67 | update: (diff: SharedDiff) => Promise.reject('random err'), 68 | deleteExpired: jest.fn(), 69 | }; 70 | const action = new CreateSharedDiffAction(repo, () => metrics); 71 | return action.execute({ diff: raw_diff }, {} as Context) 72 | .then(() => fail('should never reach')) 73 | .catch(() => { 74 | expect(repo.insert).not.toHaveBeenCalled(); 75 | expect(metrics.diffFailedToStore).toHaveBeenCalled(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /backend/tests/actions/DeleteSharedDiffAction.test.ts: -------------------------------------------------------------------------------- 1 | import { DeleteSharedDiffAction } from '../../src/actions/DeleteSharedDiffAction'; 2 | import { SharedDiffRepository } from '../../src/sharedDiffRepository/SharedDiffRepository'; 3 | import { Context, SharedDiff } from 'diffy-models'; 4 | 5 | import { metrics } from '../MockedMetrics'; 6 | 7 | test('should create a DeleteSharedDiffAction and delete a SharedDiff by id', () => { 8 | const repo: SharedDiffRepository = { 9 | insert: jest.fn(), 10 | fetchById: jest.fn(), 11 | deleteById: (id: string) => Promise.resolve(1), 12 | update: (diff: SharedDiff) => Promise.reject('random err'), 13 | deleteExpired: jest.fn(), 14 | }; 15 | const action = new DeleteSharedDiffAction(repo, () => metrics); 16 | expect(action).toBeDefined(); 17 | return action.execute({ id: '' }, {} as Context) 18 | .then(output => { 19 | expect(output.success).toEqual(true) 20 | expect(metrics.diffDeletedSuccessfully).toHaveBeenCalled() 21 | }); 22 | }); -------------------------------------------------------------------------------- /backend/tests/actions/ExtendLifetimeSharedDiffAction.test.ts: -------------------------------------------------------------------------------- 1 | import { ExtendLifetimeSharedDiffAction } from '../../src/actions/ExtendLifetimeSharedDiffAction'; 2 | import { makeSharedDiff } from '../../src/SharedDiff'; 3 | import { SharedDiffRepository } from '../../src/sharedDiffRepository/SharedDiffRepository'; 4 | import { SharedDiff } from 'diffy-models'; 5 | 6 | jest.mock('../../src/sharedDiffRepository/SharedDiffRepository'); 7 | 8 | import { metrics } from '../MockedMetrics'; 9 | 10 | const raw_diff = ` 11 | diff --git a/file.json b/file.json 12 | index 1456e89..e1da2da 100644 13 | --- a/file.json 14 | +++ b/file.json 15 | @@ -1,1 +1,1 @@ 16 | -a 17 | +b 18 | ` 19 | const DIFF = makeSharedDiff(raw_diff); 20 | const repo: SharedDiffRepository = { 21 | insert: jest.fn(), 22 | fetchById: (id: string) => Promise.resolve({ id, ...DIFF }), 23 | deleteById: (id: string) => Promise.resolve(0), 24 | update: (diff: SharedDiff) => Promise.resolve(diff), 25 | deleteExpired: jest.fn(), 26 | }; 27 | 28 | test('should make a diff permanent', () => { 29 | const spy = jest.spyOn(repo, "update"); 30 | const action = new ExtendLifetimeSharedDiffAction(repo, () => metrics); 31 | return action.execute({ id: "1" }, {} as any).then(output => { 32 | expect(spy).toHaveBeenCalled(); 33 | expect((metrics.diffLifetimeExtendedSuccessfully as any).mock.calls.length).toBe(1); 34 | expect((metrics.diffLifetimeExtendedSuccessfully as any).mock.calls[0][0]).toBe(1); 35 | expect(output.sharedDiff.expiresAt.getTime()).toBeGreaterThan(DIFF.expiresAt.getTime()); 36 | }); 37 | }); -------------------------------------------------------------------------------- /backend/tests/actions/GetSharedDiffAction.test.ts: -------------------------------------------------------------------------------- 1 | import { GetSharedDiffAction } from '../../src/actions/GetSharedDiffAction'; 2 | import { makeSharedDiff } from '../../src/SharedDiff'; 3 | import { SharedDiff } from 'diffy-models'; 4 | import { SharedDiffRepository } from '../../src/sharedDiffRepository/SharedDiffRepository'; 5 | 6 | import { metrics } from '../MockedMetrics'; 7 | 8 | test('should create a GetSharedDiffAction and fetch the SharedDiff', () => { 9 | const raw_diff = ` 10 | diff --git a/file.json b/file.json 11 | index 1456e89..e1da2da 100644 12 | --- a/file.json 13 | +++ b/file.json 14 | @@ -1,1 +1,1 @@ 15 | -a 16 | +b 17 | ` 18 | const repo: SharedDiffRepository = { 19 | insert: jest.fn(), 20 | fetchById: (id: string) => Promise.resolve({ ...makeSharedDiff(raw_diff), id }), 21 | deleteById: (id: string) => Promise.resolve(0), 22 | update: (diff: SharedDiff) => Promise.reject('random err'), 23 | deleteExpired: jest.fn(), 24 | }; 25 | const action = new GetSharedDiffAction(repo, () => metrics); 26 | expect(action).toBeDefined(); 27 | return action.execute({ id: '' }, { gaCookie: "" }).then(shared_diff => { 28 | expect(shared_diff.sharedDiff.id).toEqual(''); 29 | expect(shared_diff.sharedDiff.rawDiff).toBeDefined(); 30 | expect(metrics.diffRetrievedSuccessfully).toHaveBeenCalled() 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /backend/tests/actions/MakePermanentSharedDiffAction.ts: -------------------------------------------------------------------------------- 1 | import { makeSharedDiff } from '../../src/SharedDiff'; 2 | import { SharedDiffRepository } from '../../src/sharedDiffRepository/SharedDiffRepository'; 3 | import { SharedDiff } from 'diffy-models'; 4 | 5 | jest.mock('../../src/sharedDiffRepository/SharedDiffRepository'); 6 | 7 | import { metrics } from '../MockedMetrics'; 8 | import { MakePermanentSharedDiffAction } from '../..//src/actions/MakePermanentSharedDiffAction'; 9 | 10 | const raw_diff = ` 11 | diff --git a/file.json b/file.json 12 | index 1456e89..e1da2da 100644 13 | --- a/file.json 14 | +++ b/file.json 15 | @@ -1,1 +1,1 @@ 16 | -a 17 | +b 18 | ` 19 | const DIFF = makeSharedDiff(raw_diff); 20 | const repo: SharedDiffRepository = { 21 | insert: jest.fn(), 22 | fetchById: (id: string) => Promise.resolve({ id, ...DIFF }), 23 | deleteById: (id: string) => Promise.resolve(0), 24 | update: (diff: SharedDiff) => Promise.resolve(diff), 25 | deleteExpired: jest.fn(), 26 | }; 27 | 28 | test('should make a diff permanent', () => { 29 | const spy = jest.spyOn(repo, "update"); 30 | const action = new MakePermanentSharedDiffAction(repo, () => metrics); 31 | return action.execute({ id: "1" }, {} as any).then(output => { 32 | expect(spy).toHaveBeenCalled(); 33 | expect(output.sharedDiff.expiresAt.getFullYear()).toBe(9999); 34 | }); 35 | }); -------------------------------------------------------------------------------- /backend/tests/config.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var config = require('../src/config'); 3 | 4 | describe('Config Tests', () => { 5 | it('should read analytics key from env', () => { 6 | expect(config.GA_ANALITYCS_KEY).to.be.equal('fake key'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /backend/tests/sharedDiffRepository/MemorySharedDiffRepository.test.ts: -------------------------------------------------------------------------------- 1 | import { makeSharedDiff } from "../../src/SharedDiff"; 2 | import { MemoryDiffRepository } from "../../src/sharedDiffRepository/MemoryDiffRepository" 3 | 4 | describe('MemorySharedDiffRepository tests', () => { 5 | it("should add items", () => { 6 | const raw_diff = ` 7 | diff --git a/file.json b/file.json 8 | index 1456e89..e1da2da 100644 9 | --- a/file.json 10 | +++ b/file.json 11 | @@ -1,1 +1,1 @@ 12 | -a 13 | +b 14 | ` 15 | const shared_diff = makeSharedDiff(raw_diff); 16 | const repo = new MemoryDiffRepository(); 17 | repo.insert(shared_diff).then(diff => expect(diff.id).not.toBeNull()); 18 | }) 19 | }); -------------------------------------------------------------------------------- /backend/tests/sharedDiffRepository/MongoSharedDiffRepository.test.ts: -------------------------------------------------------------------------------- 1 | import mongodb = require('mongodb'); 2 | import { makeSharedDiff } from '../../src/SharedDiff'; 3 | import { buildDbUrl, MongoSharedDiffRepository } from '../../src/sharedDiffRepository/MongoSharedDiffRepository'; 4 | 5 | const config = { 6 | type: "mongo", 7 | db_host: process.env.DIFFY_DB_HOST || '127.0.0.1', 8 | db_port: process.env.DIFFY_DB_PORT || '27017', 9 | db_name: 'diffy', 10 | }; 11 | const db_url = buildDbUrl(config["db_host"], config["db_port"]); 12 | const client = mongodb.MongoClient.connect(db_url); 13 | const collection = client 14 | .then(client => client.db(config.db_name)) 15 | .then(db => db.collection(MongoSharedDiffRepository.COLLECTION_NAME)); 16 | 17 | describe.skip('MongoSharedDiff tests', () => { 18 | let repo: MongoSharedDiffRepository = null; 19 | beforeEach(() => { 20 | const url = db_url; 21 | const db_name = 'test'; 22 | repo = new MongoSharedDiffRepository(collection); 23 | }); 24 | 25 | afterEach(() => { 26 | client.then(c => c.close()); 27 | }); 28 | 29 | test('Mongo test: store a SharedDiff', () => { 30 | const raw_diff = ` 31 | diff --git a/file.json b/file.json 32 | index 1456e89..e1da2da 100644 33 | --- a/file.json 34 | +++ b/file.json 35 | @@ -1,1 +1,1 @@ 36 | -a 37 | +b 38 | ` 39 | const shared_diff = makeSharedDiff(raw_diff); 40 | expect(repo).toBeDefined(); 41 | return repo.insert(shared_diff).then(stored_diff => { 42 | expect(stored_diff.id).toBeDefined() 43 | expect(stored_diff.rawDiff).toBeDefined() 44 | expect(stored_diff.diff).toBeDefined() 45 | expect(stored_diff.diff).toHaveLength(0) // apparently the parsed diff is empty 46 | }); 47 | }); 48 | 49 | test('Mongo test: fetch a SharedDiff', () => { 50 | const raw_diff = ` 51 | diff --git a/file.json b/file.json 52 | index 1456e89..e1da2da 100644 53 | --- a/file.json 54 | +++ b/file.json 55 | @@ -1,1 +1,1 @@ 56 | -a 57 | +b 58 | ` 59 | const shared_diff = makeSharedDiff(raw_diff); 60 | return repo.insert(shared_diff) 61 | .then(stored_diff => repo.fetchById(stored_diff.id)) 62 | .then(shared_diff => { 63 | expect(shared_diff.id).toBeDefined() 64 | expect(shared_diff.rawDiff).toEqual(raw_diff); 65 | }); 66 | }); 67 | 68 | test('Mongo test: delete a SharedDiff', () => { 69 | const raw_diff = ` 70 | diff --git a/file.json b/file.json 71 | index 1456e89..e1da2da 100644 72 | --- a/file.json 73 | +++ b/file.json 74 | @@ -1,1 +1,1 @@ 75 | -a 76 | +b 77 | ` 78 | const shared_diff = makeSharedDiff(raw_diff); 79 | return repo.insert(shared_diff) 80 | .then(stored_diff => repo.deleteById(stored_diff.id)) 81 | .then(deletedCount => expect(deletedCount).toEqual(1)); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /backend/tests/sharedDiffRepository/SharedDiffRepository.test.ts: -------------------------------------------------------------------------------- 1 | import { MemoryDiffRepository } from "../../src/sharedDiffRepository/MemoryDiffRepository"; 2 | import { getRepositorySupplierFor } from "../../src/sharedDiffRepository/SharedDiffRepository"; 3 | import { DoubleWriteDiffRepository } from "../../src/sharedDiffRepository/DoubleWriteDiffRepository"; 4 | 5 | describe("SharedDiffRepository", () => { 6 | it("getRepositorySupplierFor double write repo", () => { 7 | const config = { 8 | type: "double_write", 9 | primary: { type: "memory" }, 10 | secondary: { type: "memory" }, 11 | } 12 | expect(getRepositorySupplierFor(config)()).toBeInstanceOf(DoubleWriteDiffRepository) 13 | }) 14 | it("getRepositorySupplierFor in memory repo", () => { 15 | const config = { 16 | type: "memory", 17 | } 18 | expect(getRepositorySupplierFor(config)()).toBeInstanceOf(MemoryDiffRepository) 19 | }) 20 | }); -------------------------------------------------------------------------------- /backend/tests/utils.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | 3 | describe('Utils module', () => { 4 | var Utils = require('../src/utils.js').Utils; 5 | 6 | describe('#getFileName()', () => { 7 | it('should return new name', () => { 8 | var file = { 9 | newName: 'newName', 10 | oldName: 'oldName' 11 | }; 12 | expect(Utils.getFileName(file)).to.be.equals('newName'); 13 | }); 14 | it('should return old name if file is deleted', () => { 15 | var file = { 16 | newName: '/dev/null', 17 | oldName: 'oldName' 18 | }; 19 | expect(Utils.getFileName(file)).to.be.equals('oldName'); 20 | }); 21 | }); 22 | 23 | describe('#sortByFilenameCriteria()', () => { 24 | it('filename a is lower than filename b', () => { 25 | var file1 = { 26 | newName: 'a', 27 | oldName: 'a' 28 | }; 29 | var file2 = { 30 | newName: 'b', 31 | oldName: 'b' 32 | }; 33 | expect(Utils.sortByFilenameCriteria(file1, file2)).to.be.equals(-1); 34 | }); 35 | it('filename b is greater than filename a', () => { 36 | var file1 = { 37 | newName: 'b', 38 | oldName: 'b' 39 | }; 40 | var file2 = { 41 | newName: 'a', 42 | oldName: 'a' 43 | }; 44 | expect(Utils.sortByFilenameCriteria(file1, file2)).to.be.equals(1); 45 | }); 46 | 47 | it('files with same filename should be equal', () => { 48 | var file1 = { 49 | newName: 'b', 50 | oldName: 'b' 51 | }; 52 | var file2 = { 53 | newName: 'b', 54 | oldName: 'b' 55 | }; 56 | expect(Utils.sortByFilenameCriteria(file1, file2)).to.be.equals(0); 57 | }); 58 | }); 59 | 60 | describe('#genRandomString()', () => { 61 | it('should return a string of length greater or equal to 10', () => { 62 | expect(Utils.genRandomString()).to.have.length.gte(10); 63 | }); 64 | }); 65 | 66 | describe('#isObjectEmpty()', () => { 67 | it('should return true for {}', () => { 68 | expect(Utils.isObjectEmpty({})).to.be.true; 69 | }); 70 | 71 | it('should return false for non empty object', () => { 72 | expect(Utils.isObjectEmpty({ x: 1 })).to.be.false; 73 | }); 74 | }); 75 | 76 | describe('#exceedsFileSizeLimit()', () => { 77 | it('should return true for files of size 1000001', () => { 78 | var file = { 79 | size: 1000001 80 | }; 81 | expect(Utils.exceedsFileSizeLimit(file)).to.be.true; 82 | }); 83 | 84 | it('should return false for files of size 1000000', () => { 85 | var file = { 86 | size: 1000000 87 | }; 88 | expect(Utils.exceedsFileSizeLimit(file)).to.be.false; 89 | }); 90 | 91 | it('should return false for files of size less than 999999', () => { 92 | var file = { 93 | size: 999999 94 | }; 95 | expect(Utils.exceedsFileSizeLimit(file)).to.be.false; 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": true, 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "baseUrl": ".", 10 | "allowJs": true, 11 | "rootDir": ".", 12 | "paths": { 13 | "*": [ 14 | "src/types/*" 15 | ] 16 | } 17 | }, 18 | "include": [ 19 | "src/**/*", 20 | "tests/**/*" 21 | ], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | web: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | image: diffy 8 | ports: 9 | - 3000:3000 10 | volumes: 11 | - ./:/diffy 12 | environment: 13 | - DIFFY_WEB_HOST=0.0.0.0 14 | - DIFFY_GA_ANALYTICS_KEY=none 15 | stop_signal: SIGINT 16 | 17 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Ngdiffy 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.0.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngdiffy": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/ngdiffy", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "extractCss": true, 42 | "namedChunks": false, 43 | "aot": true, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true, 47 | "budgets": [ 48 | { 49 | "type": "initial", 50 | "maximumWarning": "2mb", 51 | "maximumError": "5mb" 52 | } 53 | ] 54 | } 55 | } 56 | }, 57 | "serve": { 58 | "builder": "@angular-devkit/build-angular:dev-server", 59 | "options": { 60 | "browserTarget": "ngdiffy:build" 61 | }, 62 | "configurations": { 63 | "production": { 64 | "browserTarget": "ngdiffy:build:production" 65 | } 66 | } 67 | }, 68 | "extract-i18n": { 69 | "builder": "@angular-devkit/build-angular:extract-i18n", 70 | "options": { 71 | "browserTarget": "ngdiffy:build" 72 | } 73 | }, 74 | "test": { 75 | "builder": "@angular-devkit/build-angular:karma", 76 | "options": { 77 | "main": "src/test.ts", 78 | "polyfills": "src/polyfills.ts", 79 | "tsConfig": "src/tsconfig.spec.json", 80 | "karmaConfig": "src/karma.conf.js", 81 | "styles": [ 82 | "src/styles.css" 83 | ], 84 | "scripts": [], 85 | "assets": [ 86 | "src/favicon.ico", 87 | "src/assets" 88 | ] 89 | } 90 | }, 91 | "lint": { 92 | "builder": "@angular-devkit/build-angular:tslint", 93 | "options": { 94 | "tsConfig": [ 95 | "src/tsconfig.app.json", 96 | "src/tsconfig.spec.json" 97 | ], 98 | "exclude": [ 99 | "**/node_modules/**" 100 | ] 101 | } 102 | } 103 | } 104 | }, 105 | "ngdiffy-e2e": { 106 | "root": "e2e/", 107 | "projectType": "application", 108 | "prefix": "", 109 | "architect": { 110 | "e2e": { 111 | "builder": "@angular-devkit/build-angular:protractor", 112 | "options": { 113 | "protractorConfig": "e2e/protractor.conf.js", 114 | "devServerTarget": "ngdiffy:serve" 115 | }, 116 | "configurations": { 117 | "production": { 118 | "devServerTarget": "ngdiffy:serve:production" 119 | } 120 | } 121 | }, 122 | "lint": { 123 | "builder": "@angular-devkit/build-angular:tslint", 124 | "options": { 125 | "tsConfig": "e2e/tsconfig.e2e.json", 126 | "exclude": [ 127 | "**/node_modules/**" 128 | ] 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "defaultProject": "ngdiffy" 135 | } -------------------------------------------------------------------------------- /frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import {AppPage} from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to ngdiffy!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import {browser, by, element} from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngdiffy", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~12.2.2", 15 | "@angular/common": "~12.2.2", 16 | "@angular/compiler": "~12.2.2", 17 | "@angular/core": "~12.2.2", 18 | "@angular/forms": "~12.2.2", 19 | "@angular/platform-browser": "~12.2.2", 20 | "@angular/platform-browser-dynamic": "~12.2.2", 21 | "@angular/router": "~12.2.2", 22 | "core-js": "^2.5.4", 23 | "diff2html": "3.4.9", 24 | "path": "^0.12.7", 25 | "rxjs": "~6.6.7", 26 | "tslib": "^2.3.0", 27 | "zone.js": "~0.11.4", 28 | "diffy-models": "file:../models/" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~12.2.2", 32 | "@angular/cli": "~12.2.2", 33 | "@angular/compiler-cli": "~12.2.2", 34 | "@angular/language-service": "~12.2.2", 35 | "@types/jasmine": "~2.8.8", 36 | "@types/jasminewd2": "~2.0.3", 37 | "codelyzer": "~4.5.0", 38 | "jasmine-core": "~2.99.1", 39 | "jasmine-spec-reporter": "~4.2.1", 40 | "karma": "~6.3.4", 41 | "karma-chrome-launcher": "~2.2.0", 42 | "karma-coverage-istanbul-reporter": "~2.0.1", 43 | "karma-jasmine": "~1.1.2", 44 | "karma-jasmine-html-reporter": "^0.2.2", 45 | "ng-packagr": "^12.1.1", 46 | "protractor": "~7.0.0", 47 | "typescript": "4.3.5" 48 | } 49 | } -------------------------------------------------------------------------------- /frontend/src/app/Alert.ts: -------------------------------------------------------------------------------- 1 | export interface Alert { 2 | type: String, text: String, 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/app/alert.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | 3 | import {AlertService} from './alert.service'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | 6 | describe('AlertService', () => { 7 | beforeEach(() => TestBed.configureTestingModule({ 8 | imports: [RouterTestingModule], 9 | })); 10 | 11 | it('should be created', () => { 12 | const service: AlertService = TestBed.get(AlertService); 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/app/alert.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {NavigationStart, Router} from '@angular/router'; 3 | import {Observable, Subject} from 'rxjs'; 4 | 5 | interface Alert { 6 | type: String, text: String, 7 | } 8 | 9 | @Injectable({providedIn: 'root'}) 10 | export class AlertService { 11 | private subject = new Subject(); 12 | private keepAfterNavigationChange = false; 13 | 14 | constructor(private router: Router) { 15 | // clear alert message on route change 16 | router.events.subscribe(event => { 17 | if (event instanceof NavigationStart) { 18 | if (this.keepAfterNavigationChange) { 19 | // only keep for a single location change 20 | this.keepAfterNavigationChange = false; 21 | } else { 22 | // clear alert 23 | this.subject.next(); 24 | } 25 | } 26 | }); 27 | } 28 | 29 | success(message: string, keepAfterNavigationChange = false) { 30 | this.keepAfterNavigationChange = keepAfterNavigationChange; 31 | this.subject.next({type: 'success', text: message}); 32 | } 33 | 34 | error(message: string, keepAfterNavigationChange = false) { 35 | this.keepAfterNavigationChange = keepAfterNavigationChange; 36 | this.subject.next({type: 'danger', text: message}); 37 | } 38 | 39 | getMessage(): Observable { 40 | return this.subject.asObservable(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/analytics.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | 3 | import {AnalyticsService} from './analytics.service'; 4 | 5 | describe('AnalyticsService', () => { 6 | beforeEach(() => TestBed.configureTestingModule({})); 7 | 8 | it('should be created', () => { 9 | const service: AnalyticsService = TestBed.get(AnalyticsService); 10 | expect(service).toBeTruthy(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/analytics.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | 3 | @Injectable({providedIn: 'root'}) 4 | export class AnalyticsService { 5 | constructor() {} 6 | 7 | clickCopyUrlButton() { 8 | (window).ga('send', 'event', 'copyUrlButton', 'click'); 9 | } 10 | 11 | clickDownloadButton() { 12 | (window).ga('send', 'event', 'downloadButton', 'click'); 13 | } 14 | 15 | clickDeleteButton() { 16 | (window).ga('send', 'event', 'deleteButton', 'click'); 17 | } 18 | 19 | clickUploadDiffButton() { 20 | (window).ga('send', 'event', 'uploadDiffButton', 'click'); 21 | } 22 | 23 | clickDiffMeButton() { 24 | (window).ga('send', 'event', 'diffMeButton', 'click'); 25 | } 26 | 27 | clickExtendLifetimeButton() { 28 | (window).ga('send', 'event', 'extendLifetimeButton', 'click'); 29 | } 30 | 31 | clickMakePermanentButton() { 32 | (window).ga('send', 'event', 'makePermanentButton', 'click'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {CommonModule} from '@angular/common'; 2 | import {NgModule} from '@angular/core'; 3 | import {RouterModule, Routes} from '@angular/router'; 4 | 5 | import {DiffDetailComponent} from './diff-detail/diff-detail.component'; 6 | import {HomePageComponent} from './home-page/home-page.component'; 7 | 8 | 9 | const routes: Routes = [ 10 | {path: '', component: HomePageComponent, pathMatch: 'full'}, 11 | {path: 'diff/:id', component: DiffDetailComponent}, 12 | 13 | // Match all and reditect to homepage 14 | {path: '**', redirectTo: ''}, 15 | ]; 16 | 17 | @NgModule({ 18 | exports: [RouterModule], 19 | imports: [RouterModule.forRoot(routes)], 20 | }) 21 | export class AppRoutingModule { 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/app/app.component.css -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, TestBed} from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import {AppComponent} from './app.component'; 5 | 6 | describe('AppComponent', () => { 7 | beforeEach(async(() => { 8 | TestBed 9 | .configureTestingModule({ 10 | imports: [RouterTestingModule], 11 | declarations: [AppComponent], 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | it('should create the app', () => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app).toBeTruthy(); 20 | }); 21 | 22 | it(`should have as title 'ngdiffy'`, () => { 23 | const fixture = TestBed.createComponent(AppComponent); 24 | const app = fixture.debugElement.componentInstance; 25 | expect(app.title).toEqual(`Diffy - share diff output in your browser`); 26 | }); 27 | 28 | it('should render title in a h1 tag', () => { 29 | const fixture = TestBed.createComponent(AppComponent); 30 | fixture.detectChanges(); 31 | const compiled = fixture.debugElement.nativeElement; 32 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to ngdiffy!'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NavigationEnd, Router } from '@angular/router'; 3 | import { filter } from 'rxjs/operators'; 4 | 5 | @Component( 6 | { selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) 7 | export class AppComponent { 8 | title = 'Diffy'; 9 | 10 | constructor(private router: Router) { } 11 | 12 | ngOnInit() { 13 | this.router.events.pipe(filter(event => event instanceof NavigationEnd)) 14 | .subscribe((event: NavigationEnd) => { 15 | (window).ga('set', 'page', event.urlAfterRedirects); 16 | (window).ga('send', 'pageview'); 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {HttpClientModule} from '@angular/common/http'; 2 | import {HttpClient, HttpHeaders} from '@angular/common/http'; 3 | import {NgModule} from '@angular/core'; 4 | import {FormsModule} from '@angular/forms'; 5 | import {BrowserModule} from '@angular/platform-browser'; 6 | 7 | import {AppRoutingModule} from './app-routing.module'; 8 | import {AppComponent} from './app.component'; 9 | import {DiffDetailContentComponent} from './diff-detail-content/diff-detail-content.component'; 10 | import {DiffDetailCountdownComponent} from './diff-detail-countdown/diff-detail-countdown.component'; 11 | import {DiffDetailNavComponent} from './diff-detail-nav/diff-detail-nav.component'; 12 | import {DiffDetailComponent} from './diff-detail/diff-detail.component'; 13 | import {DiffFileTreeComponent} from './diff-file-tree/diff-file-tree.component'; 14 | import {HighlightComponent} from './highlight/highlight.component'; 15 | import {HomePageComponent} from './home-page/home-page.component'; 16 | import {EscapeHtmlPipe} from './pipes/keep-html.pipe'; 17 | 18 | 19 | @NgModule({ 20 | declarations: [ 21 | AppComponent, 22 | DiffDetailComponent, 23 | HomePageComponent, 24 | EscapeHtmlPipe, 25 | DiffFileTreeComponent, 26 | DiffDetailContentComponent, 27 | DiffDetailNavComponent, 28 | DiffDetailCountdownComponent, 29 | HighlightComponent, 30 | ], 31 | imports: [ 32 | BrowserModule, 33 | HttpClientModule, 34 | AppRoutingModule, 35 | FormsModule, 36 | ], 37 | providers: [], 38 | bootstrap: [AppComponent] 39 | }) 40 | export class AppModule { 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-content/diff-detail-content.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/app/diff-detail-content/diff-detail-content.component.css -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-content/diff-detail-content.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-content/diff-detail-content.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {DiffDetailContentComponent} from './diff-detail-content.component'; 4 | 5 | describe('DiffDetailContentComponent', () => { 6 | let component: DiffDetailContentComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({declarations: [DiffDetailContentComponent]}) 11 | .compileComponents(); 12 | })); 13 | 14 | beforeEach(() => { 15 | fixture = TestBed.createComponent(DiffDetailContentComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-content/diff-detail-content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges } from '@angular/core'; 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; 3 | import * as Diff2Html from 'diff2html'; 4 | 5 | import { printerUtils } from '../diff-detail/printer-utils'; 6 | import { SharedDiff } from 'diffy-models'; 7 | 8 | const DIFF2HTML_RENDER_CONFIG: Diff2Html.Diff2HtmlConfig = { 9 | drawFileList: false, 10 | matching: 'lines', 11 | }; 12 | 13 | @Component({ 14 | selector: 'app-diff-detail-content', 15 | templateUrl: './diff-detail-content.component.html', 16 | styleUrls: ['./diff-detail-content.component.css'] 17 | }) 18 | export class DiffDetailContentComponent implements OnChanges { 19 | @Input() sharedDiff: SharedDiff; 20 | @Input() fileToRender: string; 21 | diffContent: SafeHtml; 22 | 23 | constructor(private sanitizer: DomSanitizer) { } 24 | 25 | ngOnChanges(changes: any) { 26 | this.diffContent = this.sanitizer.bypassSecurityTrustHtml(this.renderDiff()); 27 | } 28 | 29 | renderDiff(): string { 30 | if (this.fileToRender) { 31 | return Diff2Html.html( 32 | this.sharedDiff.diff.filter( 33 | fileContent => printerUtils.getHtmlId(fileContent) == this.fileToRender), 34 | DIFF2HTML_RENDER_CONFIG); 35 | } 36 | return Diff2Html.html([this.sharedDiff.diff[0]], DIFF2HTML_RENDER_CONFIG); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-countdown/diff-detail-countdown.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/app/diff-detail-countdown/diff-detail-countdown.component.css -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-countdown/diff-detail-countdown.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Note: 6 | This diff will self destruct in: {{hours}} hour(s), {{minutes}} minute(s) and {{seconds}} second(s) 8 | 9 | | extend 24 hours 10 | 11 |

12 |
13 |
14 |
15 | 16 | 17 | loading ... 18 | 19 | 20 | 21 | 22 | | make permanent 23 | 24 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-countdown/diff-detail-countdown.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {DiffDetailCountdownComponent} from './diff-detail-countdown.component'; 4 | 5 | describe('DiffDetailCountdownComponent', () => { 6 | let component: DiffDetailCountdownComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({declarations: [DiffDetailCountdownComponent]}) 11 | .compileComponents(); 12 | })); 13 | 14 | beforeEach(() => { 15 | fixture = TestBed.createComponent(DiffDetailCountdownComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | 24 | it('should hide lifetime after clicked', () => { 25 | component._extendLifetime = () => {}; 26 | expect(component.extendLifetimeLoading).toBeFalsy(); 27 | expect(component.extendLifetime()) 28 | expect(component.extendLifetimeLoading).toBeTruthy(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-countdown/diff-detail-countdown.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit, SimpleChange} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-diff-detail-countdown', 5 | templateUrl: './diff-detail-countdown.component.html', 6 | styleUrls: ['./diff-detail-countdown.component.css'] 7 | }) 8 | export class DiffDetailCountdownComponent implements OnInit { 9 | @Input() expiresAt: string; 10 | @Input() displayMakePermanent: boolean; 11 | @Input() _extendLifetime: () => void; 12 | @Input() _makePermanent: () => void; 13 | @Input() diffEmail: string; 14 | hours: number; 15 | minutes: number; 16 | seconds: number; 17 | extendLifetimeLoading: boolean; 18 | 19 | constructor() {} 20 | 21 | ngOnInit() { 22 | console.log("init") 23 | setInterval(() => { 24 | this.updateTtl(); 25 | }, 1000); 26 | } 27 | 28 | ngOnChanges(changes: SimpleChange) { 29 | if(changes["expiresAt"] != undefined) { 30 | this.extendLifetimeLoading = false; 31 | } 32 | } 33 | 34 | updateTtl() { 35 | // not much scientific stuff here, just get the job done for now 36 | let now = new Date(); 37 | let dateDiffInSecs = (new Date(this.expiresAt).getTime() - now.getTime()) / 1000; 38 | if (dateDiffInSecs < 0) { 39 | this.hours = 0; 40 | this.minutes = 0; 41 | this.seconds = 0; 42 | return; 43 | } 44 | this.hours = Math.floor(dateDiffInSecs / 60 / 60); 45 | this.minutes = Math.floor((dateDiffInSecs / 60) - (this.hours * 60)); 46 | this.seconds = Math.floor((dateDiffInSecs) - (this.hours * 60 * 60) - (this.minutes * 60)); 47 | } 48 | 49 | extendLifetime() { 50 | this._extendLifetime(); 51 | this.extendLifetimeLoading = true; 52 | } 53 | 54 | makePermanent() { 55 | this._makePermanent(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-nav/diff-detail-nav.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/app/diff-detail-nav/diff-detail-nav.component.css -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-nav/diff-detail-nav.component.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-nav/diff-detail-nav.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | 3 | import {DiffDetailNavComponent} from './diff-detail-nav.component'; 4 | 5 | describe('DiffDetailNavComponent', () => { 6 | let component: DiffDetailNavComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({declarations: [DiffDetailNavComponent]}).compileComponents(); 11 | })); 12 | 13 | beforeEach(() => { 14 | fixture = TestBed.createComponent(DiffDetailNavComponent); 15 | component = fixture.componentInstance; 16 | fixture.detectChanges(); 17 | }); 18 | 19 | it('should create', () => { 20 | expect(component).toBeTruthy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail-nav/diff-detail-nav.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | 3 | import {AnalyticsService} from '../analytics.service'; 4 | 5 | @Component({ 6 | selector: 'app-diff-detail-nav', 7 | templateUrl: './diff-detail-nav.component.html', 8 | styleUrls: ['./diff-detail-nav.component.css'] 9 | }) 10 | export class DiffDetailNavComponent implements OnInit { 11 | @Input() showActions: boolean; 12 | @Input() _deleteAction: () => void; 13 | @Input() _downloadAction: () => void; 14 | @Input() _copyToClipboard: () => void; 15 | @Input() currentUrl: string; 16 | 17 | constructor(private analyticsService: AnalyticsService) {} 18 | 19 | ngOnInit() {} 20 | 21 | deleteAction() { 22 | this.analyticsService.clickDeleteButton(); 23 | this._deleteAction(); 24 | } 25 | 26 | downloadAction() { 27 | this.analyticsService.clickDownloadButton(); 28 | this._downloadAction(); 29 | } 30 | 31 | copyToClipboard() { 32 | this.analyticsService.clickCopyUrlButton(); 33 | this._copyToClipboard(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail/diff-detail.component.css: -------------------------------------------------------------------------------- 1 | .missing-diff { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail/diff-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 | 9 | 10 | 15 | 16 | 17 |
18 |
19 | 35 |
36 | 40 | 41 |
42 |
43 | 44 | 45 | 46 | 47 |
48 |
Loading ...
49 |
¯\_(ツ)_/¯ 404 Not found ...
50 | 53 |
54 |
55 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail/diff-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { ActivatedRoute, convertToParamMap } from '@angular/router'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { of } from 'rxjs'; 6 | import { AnalyticsService } from '../analytics.service'; 7 | import { DiffyService } from '../diffy.service'; 8 | 9 | import { DiffDetailComponent } from './diff-detail.component'; 10 | 11 | describe('DiffDetailComponent', () => { 12 | let component: DiffDetailComponent; 13 | let fixture: ComponentFixture; 14 | const DIFF_ID = "1"; 15 | const DIFF_CREATED_DATE = new Date('2022-01-01'); 16 | const DIFF_EXPIRES_AT = new Date('2022-01-02'); 17 | 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | imports: [RouterTestingModule, HttpClientModule], 21 | declarations: [DiffDetailComponent], 22 | providers: [ 23 | { 24 | provide: ActivatedRoute, 25 | useValue: { 26 | snapshot: { paramMap: { get: () => DIFF_ID } }, 27 | }, 28 | }, 29 | { 30 | provide: AnalyticsService, 31 | useValue: { 32 | clickMakePermanentButton: () => {}, 33 | clickExtendLifetimeButton: () => {}, 34 | } 35 | }, 36 | { 37 | provide: DiffyService, 38 | useValue: { 39 | getDiff: () => of({ 40 | id: DIFF_ID, 41 | rawDiff: "--", 42 | created: DIFF_CREATED_DATE, 43 | expiresAt: DIFF_EXPIRES_AT, 44 | diff: [ 45 | { 46 | blocks: [ /* explicitly excluded until needed */], 47 | deletedLines: 1, 48 | addedLines: 1, 49 | isGitDiff: true, 50 | checksumBefore: '1456e89', 51 | checksumAfter: 'e1da2da', 52 | mode: '100644', 53 | oldName: 'file.json', 54 | language: 'json', 55 | newName: 'file.json', 56 | isCombined: false 57 | }, 58 | { 59 | blocks: [ /* explicitly excluded until needed */], 60 | deletedLines: 1, 61 | addedLines: 1, 62 | isGitDiff: true, 63 | checksumBefore: '1456e8f', 64 | checksumAfter: 'e1da2dc', 65 | mode: '100644', 66 | oldName: 'file1.json', 67 | language: 'json', 68 | newName: 'file1.json', 69 | isCombined: false 70 | } 71 | ], 72 | }), 73 | extendLifetime: () => of({ 74 | id: DIFF_ID, 75 | rawDiff: "--", 76 | created: DIFF_CREATED_DATE, 77 | expiresAt: new Date("2022-01-03"), 78 | diff: [], 79 | }), 80 | makePermanent: () => of({ 81 | id: DIFF_ID, 82 | rawDiff: "--", 83 | created: DIFF_CREATED_DATE, 84 | expiresAt: new Date("9999-01-01"), 85 | diff: [], 86 | }), 87 | }, 88 | } 89 | ] 90 | 91 | }).compileComponents(); 92 | })); 93 | 94 | beforeEach(() => { 95 | fixture = TestBed.createComponent(DiffDetailComponent); 96 | component = fixture.componentInstance; 97 | fixture.detectChanges(); 98 | }); 99 | 100 | it('should create', () => { 101 | expect(component).toBeTruthy(); 102 | }); 103 | 104 | it('ngOnInit should fetch a diff by id', () => { 105 | expect(component.currentId).toBe(DIFF_ID); 106 | expect(component.sharedDiff.expiresAt).toEqual(DIFF_EXPIRES_AT); 107 | }); 108 | 109 | it('getExtendLifetimeFn', () => { 110 | component.getExtendLifetimeFn()(); 111 | expect(component.sharedDiff.expiresAt).toEqual(new Date("2022-01-03")); 112 | }); 113 | 114 | it('getMakePermanentDiffFn', () => { 115 | component.getMakePermanentDiffFn()("foo@example.com"); 116 | expect(component.sharedDiff.expiresAt).toEqual(new Date("9999-01-01")); 117 | }); 118 | 119 | it('selectNextFile', () => { 120 | component.selectNextFile() 121 | expect(component.selectedFileId).toEqual("d2h-397377"); 122 | component.selectNextFile() 123 | expect(component.selectedFileId).toEqual("d2h-822182"); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail/diff-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { DOCUMENT } from '@angular/common'; 2 | import { Component, Inject, Input, OnInit } from '@angular/core'; 3 | import { ActivatedRoute, Router } from '@angular/router'; 4 | 5 | import { AlertService } from '../alert.service'; 6 | import { FileTree } from '../diff-detail/tree-functions'; 7 | import { DiffyService } from '../diffy.service'; 8 | import { SharedDiff } from 'diffy-models'; 9 | import { Error } from '../types/Error'; 10 | import { AnalyticsService } from '../analytics.service'; 11 | import { printerUtils } from './printer-utils'; 12 | 13 | const DIFF_MAX_DATE = new Date('9999-01-01'); 14 | const MAKE_PERMANENT_THRESHOLD = 5 * 24 * 60 * 60 * 1000 - 1; 15 | 16 | @Component({ 17 | selector: 'app-diff-detail', 18 | templateUrl: './diff-detail.component.html', 19 | styleUrls: ['./diff-detail.component.css'] 20 | }) 21 | export class DiffDetailComponent implements OnInit { 22 | 23 | loading: boolean; 24 | sharedDiff: SharedDiff; 25 | fileTree: FileTree; 26 | fileSelectorFn: (fileId: string) => void; 27 | selectedFileId: string; 28 | dom: Document; 29 | currentId: string; 30 | fileIds: string[]; 31 | 32 | constructor( 33 | private router: Router, 34 | private route: ActivatedRoute, 35 | private diffyService: DiffyService, 36 | private alertService: AlertService, 37 | private analyticsService: AnalyticsService, 38 | @Inject(DOCUMENT) dom: Document) { 39 | 40 | this.dom = dom; 41 | } 42 | 43 | ngOnInit() { 44 | 45 | // TODO: Must refactor this into its own KeyBindingService, this way is too hacky 46 | window.addEventListener('keydown', (event: KeyboardEvent) => { 47 | if (event.key === "j") { 48 | this.selectNextFile(); 49 | } else if(event.key === "k") { 50 | this.selectPrevFile(); 51 | } 52 | }); 53 | 54 | const id = this.route.snapshot.paramMap.get('id'); 55 | this.currentId = id; 56 | this.loading = true; 57 | this.diffyService.getDiff(id).subscribe( 58 | sharedDiff => { 59 | this.sharedDiff = sharedDiff 60 | this.fileIds = printerUtils.getListOfFileIds(sharedDiff.diff) 61 | this.selectedFileId = this.fileIds[0] 62 | this.loading = false; 63 | }, 64 | error => { 65 | this.loading = false; 66 | }); 67 | } 68 | 69 | private getFileName(file) { 70 | return file.newName == '/dev/null' ? file.oldName : file.newName; 71 | } 72 | 73 | selectNextFile() : void { 74 | this.selectedFileId = this.fileIds[(this.fileIds.findIndex((element) => element == this.selectedFileId) + 1) % this.fileIds.length]; 75 | } 76 | 77 | selectPrevFile() : void { 78 | this.selectedFileId = this.fileIds[(this.fileIds.findIndex((element) => element == this.selectedFileId) - 1 + this.fileIds.length) % this.fileIds.length]; 79 | } 80 | 81 | shouldDisplayMakePermanent(): boolean { 82 | const dateDiff = this.sharedDiff.expiresAt.getTime() - this.sharedDiff.created.getTime(); 83 | return dateDiff > MAKE_PERMANENT_THRESHOLD; 84 | } 85 | 86 | getFileSelectorFn() { 87 | if (!this.fileSelectorFn) { 88 | this.fileSelectorFn = (fileId: string) => { 89 | this.selectedFileId = fileId; 90 | } 91 | } 92 | return this.fileSelectorFn; 93 | } 94 | 95 | getFileTree(): FileTree { 96 | if (!this.fileTree) { 97 | let tree = new FileTree(); 98 | this.sharedDiff.diff.forEach(e => { 99 | tree.insert(this.getFileName(e), e); 100 | }); 101 | this.fileTree = tree; 102 | } 103 | return this.fileTree; 104 | } 105 | 106 | getFileCount(): number { 107 | return this.sharedDiff.diff.length; 108 | } 109 | 110 | getDeleteDiff() { 111 | return () => { 112 | this.diffyService.deleteDiff(this.currentId) 113 | .subscribe( 114 | success => { 115 | this.alertService.success('Deleted successfully', true); 116 | this.router.navigate(['/']); 117 | }, 118 | (error: Error) => { 119 | this.alertService.error(':-( Error while deleting: ' + error.text, true); 120 | }); 121 | }; 122 | } 123 | 124 | getDownloadDiff() { 125 | return () => { 126 | this.diffyService.downloadDiff(this.currentId); 127 | }; 128 | } 129 | 130 | getExtendLifetimeFn() { 131 | return () => { 132 | this.analyticsService.clickExtendLifetimeButton(); 133 | this.diffyService.extendLifetime(this.currentId) 134 | .subscribe( 135 | sharedDiff => { 136 | this.sharedDiff = sharedDiff; 137 | }, 138 | (error: Error) => { 139 | this.alertService.error(':-( Error while extending diff: ' + error.text, true); 140 | }); 141 | }; 142 | } 143 | 144 | getMakePermanentDiffFn(): (email: string) => void { 145 | return () => { 146 | this.analyticsService.clickMakePermanentButton(); 147 | this.diffyService.makePermanent(this.currentId) 148 | .subscribe( 149 | sharedDiff => { 150 | this.sharedDiff = sharedDiff; 151 | }, 152 | (error: Error) => { 153 | this.alertService.error(':-( Error while making diff permanent: ' + error.text, true); 154 | }); 155 | }; 156 | } 157 | 158 | getCopyUrlToClipboard() { 159 | return () => { 160 | // copy logic here 161 | let element = this.dom.getElementById('clip-txt') as any; 162 | element.select(); 163 | this.dom.execCommand('copy'); 164 | }; 165 | } 166 | 167 | getCurrentUrl() { 168 | return window.location.href; 169 | } 170 | 171 | isDiffPermanent() { 172 | return this.sharedDiff.expiresAt >= DIFF_MAX_DATE; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail/printer-utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * PrinterUtils (printer-utils.js) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | 9 | var separator = '/'; 10 | 11 | function PrinterUtils() {} 12 | 13 | PrinterUtils.prototype.separatePrefix = function(isCombined, line) { 14 | var prefix; 15 | var lineWithoutPrefix; 16 | 17 | if (isCombined) { 18 | prefix = line.substring(0, 2); 19 | lineWithoutPrefix = line.substring(2); 20 | } else { 21 | prefix = line.substring(0, 1); 22 | lineWithoutPrefix = line.substring(1); 23 | } 24 | 25 | return {'prefix': prefix, 'line': lineWithoutPrefix}; 26 | }; 27 | 28 | /** 29 | * diff: DiffFile[] 30 | */ 31 | PrinterUtils.prototype.getListOfFileIds = function(diff) { 32 | var fileIds = []; 33 | for(var i=0; i < diff.length; i++) { 34 | fileIds.push(this.getHtmlId(diff[i])); 35 | } 36 | return fileIds; 37 | } 38 | 39 | PrinterUtils.prototype.getHtmlId = function(file) { 40 | var hashCode = function(text) { 41 | var i, chr, len; 42 | var hash = 0; 43 | 44 | for (i = 0, len = text.length; i < len; i++) { 45 | chr = text.charCodeAt(i); 46 | hash = ((hash << 5) - hash) + chr; 47 | hash |= 0; // Convert to 32bit integer 48 | } 49 | 50 | return hash; 51 | }; 52 | 53 | return 'd2h-' + hashCode(this.getDiffName(file)).toString().slice(-6); 54 | }; 55 | 56 | PrinterUtils.prototype.getDiffName = function(file) { 57 | var oldFilename = unifyPath(file.oldName); 58 | var newFilename = unifyPath(file.newName); 59 | 60 | if (oldFilename && newFilename && oldFilename !== newFilename && !isDevNullName(oldFilename) && 61 | !isDevNullName(newFilename)) { 62 | var prefixPaths = []; 63 | var suffixPaths = []; 64 | 65 | var oldFilenameParts = oldFilename.split(separator); 66 | var newFilenameParts = newFilename.split(separator); 67 | 68 | var oldFilenamePartsSize = oldFilenameParts.length; 69 | var newFilenamePartsSize = newFilenameParts.length; 70 | 71 | var i = 0; 72 | var j = oldFilenamePartsSize - 1; 73 | var k = newFilenamePartsSize - 1; 74 | 75 | while (i < j && i < k) { 76 | if (oldFilenameParts[i] === newFilenameParts[i]) { 77 | prefixPaths.push(newFilenameParts[i]); 78 | i += 1; 79 | } else { 80 | break; 81 | } 82 | } 83 | 84 | while (j > i && k > i) { 85 | if (oldFilenameParts[j] === newFilenameParts[k]) { 86 | suffixPaths.unshift(newFilenameParts[k]); 87 | j -= 1; 88 | k -= 1; 89 | } else { 90 | break; 91 | } 92 | } 93 | 94 | var finalPrefix = prefixPaths.join(separator); 95 | var finalSuffix = suffixPaths.join(separator); 96 | 97 | var oldRemainingPath = oldFilenameParts.slice(i, j + 1).join(separator); 98 | var newRemainingPath = newFilenameParts.slice(i, k + 1).join(separator); 99 | 100 | if (finalPrefix.length && finalSuffix.length) { 101 | return finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + 102 | separator + finalSuffix; 103 | } else if (finalPrefix.length) { 104 | return finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}'; 105 | } else if (finalSuffix.length) { 106 | return '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix; 107 | } 108 | 109 | return oldFilename + ' → ' + newFilename; 110 | } else if (newFilename && !isDevNullName(newFilename)) { 111 | return newFilename; 112 | } else if (oldFilename) { 113 | return oldFilename; 114 | } 115 | 116 | return 'unknown/file/path'; 117 | }; 118 | 119 | function unifyPath(path) { 120 | if (path) { 121 | return path.replace('\\', '/'); 122 | } 123 | 124 | return path; 125 | } 126 | 127 | function isDevNullName(name) { 128 | return name.indexOf('dev/null') !== -1; 129 | } 130 | 131 | export const printerUtils = new PrinterUtils(); 132 | -------------------------------------------------------------------------------- /frontend/src/app/diff-detail/tree-functions.ts: -------------------------------------------------------------------------------- 1 | /* rough stuff, but for now I think will do 2 | * 3 | * Is basically a compressed TRIE datastructure where 4 | * the nodes represents members of the file system either 5 | * directories (subtrees) or files (leaves) 6 | * 7 | * */ 8 | 9 | import {printerUtils} from './printer-utils'; 10 | 11 | 12 | export class FileTree { 13 | public parent: any; 14 | public path: any; 15 | public files: any; 16 | public dirs: any; 17 | public value: any; 18 | public fileId: string; 19 | 20 | constructor(parent = null, filename = '/', value = null) { 21 | this.parent = parent || null; 22 | this.path = filename || '/'; 23 | this.files = []; 24 | this.dirs = []; 25 | this.value = value; 26 | } 27 | 28 | public createLeaf(parent, filename, value) { 29 | return new FileTree(parent, filename, value); 30 | }; 31 | 32 | _getCommonPrefixIndex(pathSplit, dirs) { 33 | var l = Math.min(pathSplit.length, dirs.length); 34 | var i = 0; 35 | for (; i < l; i++) { 36 | if (pathSplit[i] != dirs[i]) { 37 | return i; 38 | } 39 | } 40 | return i; 41 | }; 42 | 43 | _insertAsSubtree(dirs, file, value) { 44 | for (var i = 0; i < this.dirs.length; i++) { 45 | var subtree = this.dirs[i]; 46 | var split = subtree.path.split('/'); 47 | if (split[0] == dirs[0]) { 48 | subtree._insert(dirs, file, value); 49 | return; 50 | } 51 | } 52 | // no common prefix, just insert 53 | var nTree = new FileTree(); 54 | nTree.path = dirs.join('/'); 55 | nTree.files.push(this.createLeaf(nTree, file, value)); 56 | this.dirs.push(nTree); 57 | nTree.parent = this; 58 | } 59 | 60 | _insert(dirs, file, value) { 61 | if (dirs.length === 0) { 62 | this.files.push(this.createLeaf(this, file, value)); 63 | return; 64 | } 65 | var pathSplit = this.path.split('/'); 66 | var i = this._getCommonPrefixIndex(pathSplit, dirs); 67 | if (i < pathSplit.length && this.path != '/') { 68 | // split at i 69 | var tree_dirs = this.dirs; 70 | var tree_files = this.files; 71 | 72 | var dirsSuffix = dirs.slice(i).join('/'); // new tree suffix 73 | var commonPrefix = dirs.slice(0, i); // common prefix (array) 74 | 75 | 76 | var convertedSubTree = new FileTree(); 77 | convertedSubTree.path = pathSplit.slice(i).join('/'); // current tree suffix 78 | convertedSubTree.dirs = tree_dirs; 79 | convertedSubTree.files = tree_files; 80 | convertedSubTree.dirs.forEach(function(tree) { 81 | tree.parent = convertedSubTree; 82 | }); 83 | convertedSubTree.files.forEach(function(leaf) { 84 | leaf.parent = convertedSubTree; 85 | }); 86 | 87 | this.path = commonPrefix.join('/'); 88 | this.dirs = [convertedSubTree]; 89 | this.files = []; 90 | 91 | // If the new subtree is a file 92 | if (dirsSuffix == '') { 93 | // Appending the new leaf 94 | leaf = this.createLeaf(this, file, value); 95 | this.files.push(leaf); 96 | return; 97 | } 98 | 99 | var nSubTree = new FileTree(); 100 | nSubTree.path = dirsSuffix; 101 | nSubTree.files.push(this.createLeaf(nSubTree, file, value)); 102 | 103 | // Adding the new dir 104 | this.dirs.push(nSubTree); 105 | 106 | // update parents 107 | convertedSubTree.parent = this; 108 | nSubTree.parent = this; 109 | } else { 110 | var slice = dirs.slice(i); 111 | if (slice.length == 0) { 112 | // same path, only insert file 113 | var leaf = this.createLeaf(this, file, value); 114 | this.files.push(leaf); 115 | } else { 116 | // find the proper subdirectory to continue 117 | this._insertAsSubtree(slice, file, value); 118 | } 119 | } 120 | } 121 | 122 | insert(file, value) { 123 | if (file[0] != '/') { 124 | file = '/' + file; 125 | } 126 | var dirs = file.split('/'); 127 | file = dirs.pop(); 128 | this._insert(dirs, file, value); 129 | } 130 | 131 | getFileName(node) { 132 | var res = node.path; 133 | while (node.parent !== null && node.parent.path != '/') { 134 | node = node.parent; 135 | res = node.path + '/' + res; 136 | } 137 | return '/' + res; 138 | } 139 | 140 | printTree(tree, level) { 141 | var space = ''; 142 | var spaceStr = ' '; 143 | for (var i = 0; i < level; i++) space += spaceStr; 144 | var result = ''; 145 | result += space + '
    \n'; 146 | space = space + spaceStr; 147 | result += space + '
  • \n'; 148 | result += space + 149 | '' + 150 | tree.path + '\n'; 151 | var that = this; 152 | if (tree.dirs.length > 0) { 153 | tree.dirs.forEach(function(subtree) { 154 | result += that.printTree(subtree, level + 1); 155 | }); 156 | } 157 | if (tree.files.length > 0) { 158 | result += space + '
      \n'; 159 | tree.files.forEach(function(file) { 160 | var filename = that.getFileName(file); 161 | var id = printerUtils.getHtmlId(file.value); 162 | result += space + spaceStr; 163 | result += '
    • ' + 164 | ' \n' + 166 | ' ' + file.path + 167 | '\n' + 168 | ' \n' + 169 | '
    • '; 170 | }); 171 | result += space + '
    \n'; 172 | } 173 | result += space + '
  • \n'; 174 | space = ''; 175 | for (i = 0; i < level; i++) space += spaceStr; 176 | result += space + '
\n'; 177 | return result; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /frontend/src/app/diff-file-tree/diff-file-tree.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/app/diff-file-tree/diff-file-tree.component.css -------------------------------------------------------------------------------- /frontend/src/app/diff-file-tree/diff-file-tree.component.html: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | 35 | {{fileTree.path}} 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/app/diff-file-tree/diff-file-tree.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FileTree } from '../diff-detail/tree-functions'; 3 | 4 | import { DiffFileTreeComponent } from './diff-file-tree.component'; 5 | 6 | describe('DiffFileTreeComponent', () => { 7 | let component: DiffFileTreeComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({declarations: [DiffFileTreeComponent]}).compileComponents(); 12 | })); 13 | 14 | beforeEach(() => { 15 | fixture = TestBed.createComponent(DiffFileTreeComponent); 16 | component = fixture.componentInstance; 17 | component.fileTree = new FileTree(); 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /frontend/src/app/diff-file-tree/diff-file-tree.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | 3 | import {printerUtils} from '../diff-detail/printer-utils'; 4 | import {FileTree} from '../diff-detail/tree-functions'; 5 | 6 | @Component({ 7 | selector: 'app-diff-file-tree', 8 | templateUrl: './diff-file-tree.component.html', 9 | styleUrls: ['./diff-file-tree.component.css'] 10 | }) 11 | export class DiffFileTreeComponent implements OnInit { 12 | 13 | @Input() fileTree: FileTree; 14 | @Input() isOpen: boolean; 15 | @Input() fileSelectorFn: (fileId: string) => void; 16 | @Input() selectedFile: string; 17 | 18 | constructor() {} 19 | 20 | ngOnInit() {} 21 | 22 | private getFileName(file) { 23 | return file.newName == '/dev/null' ? file.oldName : file.newName; 24 | }; 25 | 26 | getFileHash(file) { 27 | return printerUtils.getHtmlId(file.value); 28 | } 29 | 30 | toggleFolder() { 31 | this.isOpen = !this.isOpen; 32 | } 33 | 34 | isFileSelected(file) { 35 | return this.getFileHash(file) == this.selectedFile; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/src/app/diffy.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import {TestBed} from '@angular/core/testing'; 3 | 4 | import {DiffyService} from './diffy.service'; 5 | 6 | describe('DiffyService', () => { 7 | beforeEach(() => TestBed.configureTestingModule({ 8 | imports: [HttpClientModule], 9 | })); 10 | 11 | it('should be created', () => { 12 | const service: DiffyService = TestBed.get(DiffyService); 13 | expect(service).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/app/diffy.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; 2 | import {Injectable} from '@angular/core'; 3 | import {Observable, throwError} from 'rxjs'; 4 | import {catchError, map} from 'rxjs/operators'; 5 | import { SharedDiff } from 'diffy-models'; 6 | import {Error} from './types/Error'; 7 | import * as Diff2Html from 'diff2html'; 8 | 9 | // var diff2html = require('diff2html'); 10 | 11 | @Injectable({providedIn: 'root'}) 12 | export class DiffyService { 13 | private diffyUrl = '/api/diff/'; // URL to web api 14 | 15 | constructor(private http: HttpClient) {} 16 | 17 | private handleError(operation = 'operation', result?: T) { 18 | return (error: any): Observable => { 19 | return throwError(this.buildError(error)); 20 | }; 21 | } 22 | 23 | private buildError(httpError): Error { 24 | if (httpError.status >= 500) { 25 | return {type: 'SERVER_ERROR', text: 'Oops, something broke on the server :-/'}; 26 | } 27 | if (httpError.status >= 400) { 28 | return { 29 | type: 'CLIENT_ERROR', 30 | text: httpError.error.error || 'unknown error', 31 | }; 32 | } 33 | } 34 | 35 | getDiff(id: string): Observable { 36 | return this.http.get(this.diffyUrl + id) 37 | .pipe(map((getDiffOutput: any) => this.makeSharedDiffFromJson(getDiffOutput._sharedDiff))) 38 | .pipe(catchError(this.handleError('getDiff', null))); 39 | } 40 | 41 | storeDiff(diffText: string): Observable { 42 | const httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; 43 | 44 | return this.http.put(this.diffyUrl, {diff: diffText}, httpOptions) 45 | .pipe(map((createDiffOutput: any) => this.makeSharedDiffFromJson(createDiffOutput._sharedDiff))) 46 | .pipe(catchError(this.handleError('getDiff', null))); 47 | } 48 | 49 | deleteDiff(id: string): Observable { 50 | const httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; 51 | 52 | return this.http.delete(this.diffyUrl + id, httpOptions) 53 | .pipe(map((deleteDiffOutput: any) => deleteDiffOutput._success)) 54 | .pipe(catchError(this.handleError('deleteDiff', null))); 55 | } 56 | 57 | extendLifetime(id: string): Observable { 58 | const httpOptions = {headers: new HttpHeaders({'Content-Type': 'application/json'})}; 59 | 60 | return this.http.post(this.diffyUrl + 'extend/' + id, httpOptions) 61 | .pipe(map((extendLifetimeOutput: any) => this.makeSharedDiffFromJson(extendLifetimeOutput._sharedDiff))) 62 | .pipe(catchError(this.handleError('extendLifetimeDiff', null))); 63 | } 64 | 65 | makePermanent(id: string): Observable { 66 | const httpOptions = { 67 | headers: new HttpHeaders({'Content-Type': 'application/json'}), 68 | }; 69 | 70 | return this.http.post(this.diffyUrl + 'makePermanent/' + id, httpOptions) 71 | .pipe(map((makePermanentOutput: any) => this.makeSharedDiffFromJson(makePermanentOutput._sharedDiff))) 72 | .pipe(catchError(this.handleError('extendLifetimeDiff', null))); 73 | } 74 | 75 | downloadDiff(id: string) { 76 | window.open('/diff_download/' + id); 77 | } 78 | 79 | private makeSharedDiff(raw_diff: string, date: Date = new Date()): SharedDiff { 80 | let expire_date = new Date(); 81 | expire_date.setDate(date.getDate() + 1); 82 | return { 83 | created: date, 84 | expiresAt: expire_date, 85 | diff: Diff2Html.parse(raw_diff), 86 | rawDiff: raw_diff, 87 | }; 88 | } 89 | 90 | private makeSharedDiffFromJson(diffyObj): SharedDiff { 91 | let sharedDiff = this.makeSharedDiff(diffyObj.rawDiff, new Date(diffyObj.created)); 92 | sharedDiff.expiresAt = new Date(diffyObj.expiresAt); 93 | sharedDiff.id = diffyObj.id; 94 | return sharedDiff 95 | } 96 | } -------------------------------------------------------------------------------- /frontend/src/app/highlight/highlight.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/app/highlight/highlight.component.css -------------------------------------------------------------------------------- /frontend/src/app/highlight/highlight.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Error: 5 | 6 | {{alert.text}} 7 |
8 | -------------------------------------------------------------------------------- /frontend/src/app/highlight/highlight.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import {HighlightComponent} from './highlight.component'; 5 | 6 | describe('HighlightComponent', () => { 7 | let component: HighlightComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | imports: [RouterTestingModule], 13 | declarations: [HighlightComponent] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(HighlightComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /frontend/src/app/highlight/highlight.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | 3 | import {Alert} from '../Alert'; 4 | import {AlertService} from '../alert.service'; 5 | 6 | @Component({ 7 | selector: 'app-highlight', 8 | templateUrl: './highlight.component.html', 9 | styleUrls: ['./highlight.component.css'] 10 | }) 11 | export class HighlightComponent implements OnInit { 12 | alert: Alert; 13 | 14 | constructor(private alertService: AlertService) {} 15 | 16 | ngOnInit() { 17 | this.alertService.getMessage().subscribe(alert => { 18 | this.alert = alert; 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/home-page/home-page.component.css: -------------------------------------------------------------------------------- 1 | span.hljs { 2 | display: inherit; 3 | overflow-x: inherit; 4 | padding: inherit; 5 | color: #333; 6 | background: inherit; 7 | } 8 | 9 | .no-pointer-events { 10 | pointer-events: none; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/home-page/home-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Diffy - A tool for sharing diffs

5 |

Share your diffs and explain your ideas without committing

6 |
7 |
8 |
9 |
10 |

Mac:

11 |
    12 |
  1. git diff | pbcopy and paste it on the textarea.
  2. 13 |
  3. Click "Diff me"
  4. 14 |
15 |
16 |
17 |

Linux:

18 |
    19 |
  1. git diff > output.diff
  2. 20 |
  3. upload the output.diff file.
  4. 21 |
22 |
23 |
24 |

Windows:

25 |
    26 |
  1. git diff | clip and paste it on the textarea.
  2. 27 |
  3. Click "Diff me"
  4. 28 |
29 |
30 |
31 |

Note: It's not only restricted to git diff. Anything with a "diffy" output can be shown, like git show <commit number>

32 |
33 |
34 | 36 |
37 |
38 | Diff me 39 | 40 | Upload Diff 41 | 42 |
43 |
44 |
45 | 46 | 47 |
48 |
49 |

50 |

Share your ideas more clearly

51 |

52 | Show your awesome improvement idea to your colleagues before committing 53 |

54 |
55 |
56 |

57 |

Get feedback from your peers

58 |

Hear the feedback and react on it before having to fix buggy commits

59 |
60 |
61 |

62 |

Discover bugs sooner

63 |

Show to your friends how would you fix the bug they have in production

64 |
65 |
66 | 67 |
68 |
69 |
70 | -------------------------------------------------------------------------------- /frontend/src/app/home-page/home-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | 5 | import {HomePageComponent} from './home-page.component'; 6 | 7 | describe('HomePageComponent', () => { 8 | let component: HomePageComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | imports: [RouterTestingModule, HttpClientModule], 14 | declarations: [HomePageComponent] 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(HomePageComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /frontend/src/app/home-page/home-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { of } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { AlertService } from '../alert.service'; 7 | import { AnalyticsService } from '../analytics.service'; 8 | import { DiffyService } from '../diffy.service'; 9 | import { Error } from '../types/Error'; 10 | 11 | @Component({ 12 | selector: 'app-home-page', 13 | templateUrl: './home-page.component.html', 14 | styleUrls: ['./home-page.component.css'] 15 | }) 16 | export class HomePageComponent implements OnInit { 17 | @Input() diffText: string; 18 | private uploading: boolean; 19 | 20 | constructor( 21 | private router: Router, private diffyService: DiffyService, 22 | private alertService: AlertService, private analyticsService: AnalyticsService) { 23 | 24 | this.uploading = false; 25 | } 26 | 27 | ngOnInit() { } 28 | 29 | isUploading(): boolean { 30 | return this.uploading; 31 | } 32 | 33 | diffMeClick() { 34 | this.analyticsService.clickDiffMeButton(); 35 | this.submitDiff(); 36 | } 37 | 38 | submitDiff() { 39 | this.uploading = true; 40 | this.diffyService.storeDiff(this.diffText) 41 | .subscribe( 42 | sharedDiff => { 43 | this.router.navigate([`/diff/${sharedDiff.id}`]) 44 | this.uploading = false; 45 | }, 46 | (error: Error) => { 47 | this.alertService.error('Error: ' + error.text); 48 | this.uploading = false; 49 | }); 50 | } 51 | 52 | uploadChange(fileInput: Event) { 53 | this.analyticsService.clickUploadDiffButton(); 54 | let file = (fileInput.target as any).files[0]; 55 | let reader = new FileReader(); 56 | reader.onload = (e) => { 57 | this.diffText = (e.target as any).result; 58 | this.submitDiff(); 59 | }; 60 | reader.readAsText(file); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/app/pipes/keep-html.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | import {DomSanitizer} from '@angular/platform-browser'; 3 | 4 | @Pipe({name: 'keepHtml', pure: false}) 5 | export class EscapeHtmlPipe implements PipeTransform { 6 | constructor(private sanitizer: DomSanitizer) {} 7 | 8 | transform(content) { 9 | return this.sanitizer.bypassSecurityTrustHtml(content); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/types/Error.ts: -------------------------------------------------------------------------------- 1 | export interface Error { 2 | type: 'CLIENT_ERROR'|'SERVER_ERROR'; 3 | text: string; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/assets/.gitkeep -------------------------------------------------------------------------------- /frontend/src/assets/diff.css: -------------------------------------------------------------------------------- 1 | td, th { 2 | padding: 1px; 3 | } 4 | 5 | #diff-wrapper { 6 | min-width: 70%; 7 | position: relative; 8 | overflow: hidden; 9 | } 10 | 11 | .diff-content { 12 | padding: 5px; 13 | } 14 | 15 | .clipboard-group{ 16 | width: 300px; 17 | display: inline-block; 18 | vertical-align: middle; 19 | } 20 | 21 | #clip-txt{ 22 | width: 261px; 23 | } 24 | 25 | .directory, .file, .dir-name, .file-name { 26 | white-space: nowrap; 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/assets/diff2html.css: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Diff to HTML (diff2html.css) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | .d2h-wrapper { 9 | text-align: left; 10 | } 11 | 12 | .d2h-file-header { 13 | padding: 5px 10px; 14 | border-bottom: 1px solid #d8d8d8; 15 | background-color: #f7f7f7; 16 | } 17 | 18 | .d2h-file-stats { 19 | display: -webkit-box; 20 | display: -ms-flexbox; 21 | display: flex; 22 | margin-left: auto; 23 | font-size: 14px; 24 | } 25 | 26 | .d2h-lines-added { 27 | text-align: right; 28 | border: 1px solid #b4e2b4; 29 | border-radius: 5px 0 0 5px; 30 | color: #399839; 31 | padding: 2px; 32 | vertical-align: middle; 33 | } 34 | 35 | .d2h-lines-deleted { 36 | text-align: left; 37 | border: 1px solid #e9aeae; 38 | border-radius: 0 5px 5px 0; 39 | color: #c33; 40 | padding: 2px; 41 | vertical-align: middle; 42 | margin-left: 1px; 43 | } 44 | 45 | .d2h-file-name-wrapper { 46 | display: -webkit-box; 47 | display: -ms-flexbox; 48 | display: flex; 49 | -webkit-box-align: center; 50 | -ms-flex-align: center; 51 | align-items: center; 52 | width: 100%; 53 | font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; 54 | font-size: 15px; 55 | } 56 | 57 | .d2h-file-name { 58 | white-space: nowrap; 59 | text-overflow: ellipsis; 60 | overflow-x: hidden; 61 | line-height: 21px; 62 | } 63 | 64 | .d2h-file-wrapper { 65 | border: 1px solid #ddd; 66 | border-radius: 3px; 67 | margin-bottom: 1em; 68 | } 69 | 70 | .d2h-diff-table { 71 | width: 100%; 72 | border-collapse: collapse; 73 | font-family: "Menlo", "Consolas", monospace; 74 | font-size: 13px; 75 | } 76 | 77 | .d2h-diff-tbody > tr > td { 78 | height: 20px; 79 | line-height: 20px; 80 | } 81 | 82 | .d2h-files-diff { 83 | display: block; 84 | width: 100%; 85 | height: 100%; 86 | } 87 | 88 | .d2h-file-diff { 89 | overflow-x: scroll; 90 | overflow-y: hidden; 91 | } 92 | 93 | .d2h-file-side-diff { 94 | display: inline-block; 95 | overflow-x: scroll; 96 | overflow-y: hidden; 97 | width: 50%; 98 | margin-right: -4px; 99 | margin-bottom: -8px; 100 | } 101 | 102 | .d2h-code-line { 103 | display: inline-block; 104 | white-space: nowrap; 105 | padding: 0 10px; 106 | margin-left: 80px; 107 | } 108 | 109 | .d2h-code-side-line { 110 | display: inline-block; 111 | white-space: nowrap; 112 | padding: 0 10px; 113 | margin-left: 50px; 114 | } 115 | 116 | .d2h-code-line del, 117 | .d2h-code-side-line del { 118 | display: inline-block; 119 | margin-top: -1px; 120 | text-decoration: none; 121 | background-color: #ffb6ba; 122 | border-radius: 0.2em; 123 | } 124 | 125 | .d2h-code-line ins, 126 | .d2h-code-side-line ins { 127 | display: inline-block; 128 | margin-top: -1px; 129 | text-decoration: none; 130 | background-color: #97f295; 131 | border-radius: 0.2em; 132 | text-align: left; 133 | } 134 | 135 | .d2h-code-line-prefix { 136 | display: inline; 137 | background: none; 138 | padding: 0; 139 | word-wrap: normal; 140 | white-space: pre; 141 | } 142 | 143 | .d2h-code-line-ctn { 144 | display: inline; 145 | background: none; 146 | padding: 0; 147 | word-wrap: normal; 148 | white-space: pre; 149 | } 150 | 151 | .line-num1 { 152 | box-sizing: border-box; 153 | float: left; 154 | width: 40px; 155 | overflow: hidden; 156 | text-overflow: ellipsis; 157 | padding-left: 3px; 158 | } 159 | 160 | .line-num2 { 161 | box-sizing: border-box; 162 | float: right; 163 | width: 40px; 164 | overflow: hidden; 165 | text-overflow: ellipsis; 166 | padding-left: 3px; 167 | } 168 | 169 | .d2h-code-linenumber { 170 | box-sizing: border-box; 171 | position: absolute; 172 | width: 86px; 173 | padding-left: 2px; 174 | padding-right: 2px; 175 | background-color: #fff; 176 | color: rgba(0, 0, 0, 0.3); 177 | text-align: right; 178 | border: solid #eeeeee; 179 | border-width: 0 1px 0 1px; 180 | cursor: pointer; 181 | } 182 | 183 | .d2h-code-side-linenumber { 184 | box-sizing: border-box; 185 | position: absolute; 186 | width: 56px; 187 | padding-left: 5px; 188 | padding-right: 5px; 189 | background-color: #fff; 190 | color: rgba(0, 0, 0, 0.3); 191 | text-align: right; 192 | border: solid #eeeeee; 193 | border-width: 0 1px 0 1px; 194 | cursor: pointer; 195 | overflow: hidden; 196 | text-overflow: ellipsis; 197 | } 198 | 199 | /* 200 | * Changes Highlight 201 | */ 202 | 203 | .d2h-del { 204 | background-color: #fee8e9; 205 | border-color: #e9aeae; 206 | } 207 | 208 | .d2h-ins { 209 | background-color: #dfd; 210 | border-color: #b4e2b4; 211 | } 212 | 213 | .d2h-info { 214 | background-color: #f8fafd; 215 | color: rgba(0, 0, 0, 0.3); 216 | border-color: #d5e4f2; 217 | } 218 | 219 | .d2h-file-diff .d2h-del.d2h-change { 220 | background-color: #fdf2d0; 221 | } 222 | 223 | .d2h-file-diff .d2h-ins.d2h-change { 224 | background-color: #ded; 225 | } 226 | 227 | /* 228 | * File Summary List 229 | */ 230 | 231 | .d2h-file-list-wrapper { 232 | margin-bottom: 10px; 233 | } 234 | 235 | .d2h-file-list-wrapper a { 236 | text-decoration: none; 237 | color: #3572b0; 238 | } 239 | 240 | .d2h-file-list-wrapper a:visited { 241 | color: #3572b0; 242 | } 243 | 244 | .d2h-file-list-header { 245 | text-align: left; 246 | } 247 | 248 | .d2h-file-list-title { 249 | font-weight: bold; 250 | } 251 | 252 | .d2h-file-list-line { 253 | display: -webkit-box; 254 | display: -ms-flexbox; 255 | display: flex; 256 | text-align: left; 257 | } 258 | 259 | .d2h-file-list { 260 | display: block; 261 | list-style: none; 262 | padding: 0; 263 | margin: 0; 264 | } 265 | 266 | .d2h-file-list > li { 267 | border-bottom: #ddd solid 1px; 268 | padding: 5px 10px; 269 | margin: 0; 270 | } 271 | 272 | .d2h-file-list > li:last-child { 273 | border-bottom: none; 274 | } 275 | 276 | .d2h-file-switch { 277 | display: none; 278 | font-size: 10px; 279 | cursor: pointer; 280 | } 281 | 282 | .d2h-icon-wrapper { 283 | line-height: 31px; 284 | } 285 | 286 | .d2h-icon { 287 | vertical-align: middle; 288 | margin-right: 10px; 289 | fill: currentColor; 290 | } 291 | 292 | .d2h-deleted { 293 | color: #c33; 294 | } 295 | 296 | .d2h-added { 297 | color: #399839; 298 | } 299 | 300 | .d2h-changed { 301 | color: #d0b44c; 302 | } 303 | 304 | .d2h-moved { 305 | color: #3572b0; 306 | } 307 | 308 | .d2h-tag { 309 | display: -webkit-box; 310 | display: -ms-flexbox; 311 | display: flex; 312 | font-size: 10px; 313 | margin-left: 5px; 314 | padding: 0 2px; 315 | background-color: #fff; 316 | } 317 | 318 | .d2h-deleted-tag { 319 | border: #c33 1px solid; 320 | } 321 | 322 | .d2h-added-tag { 323 | border: #399839 1px solid; 324 | } 325 | 326 | .d2h-changed-tag { 327 | border: #d0b44c 1px solid; 328 | } 329 | 330 | .d2h-moved-tag { 331 | border: #3572b0 1px solid; 332 | } 333 | 334 | /* 335 | * Selection util. 336 | */ 337 | 338 | .selecting-left .d2h-code-line, 339 | .selecting-left .d2h-code-line *, 340 | .selecting-right td.d2h-code-linenumber, 341 | .selecting-right td.d2h-code-linenumber *, 342 | .selecting-left .d2h-code-side-line, 343 | .selecting-left .d2h-code-side-line *, 344 | .selecting-right td.d2h-code-side-linenumber, 345 | .selecting-right td.d2h-code-side-linenumber * { 346 | -webkit-touch-callout: none; 347 | -webkit-user-select: none; 348 | -moz-user-select: none; 349 | -ms-user-select: none; 350 | user-select: none; 351 | } 352 | 353 | .selecting-left .d2h-code-line::-moz-selection, 354 | .selecting-left .d2h-code-line *::-moz-selection, 355 | .selecting-right td.d2h-code-linenumber::-moz-selection, 356 | .selecting-left .d2h-code-side-line::-moz-selection, 357 | .selecting-left .d2h-code-side-line *::-moz-selection, 358 | .selecting-right td.d2h-code-side-linenumber::-moz-selection, 359 | .selecting-right td.d2h-code-side-linenumber *::-moz-selection { 360 | background: transparent; 361 | } 362 | 363 | .selecting-left .d2h-code-line::selection, 364 | .selecting-left .d2h-code-line *::selection, 365 | .selecting-right td.d2h-code-linenumber::selection, 366 | .selecting-left .d2h-code-side-line::selection, 367 | .selecting-left .d2h-code-side-line *::selection, 368 | .selecting-right td.d2h-code-side-linenumber::selection, 369 | .selecting-right td.d2h-code-side-linenumber *::selection { 370 | background: transparent; 371 | } 372 | -------------------------------------------------------------------------------- /frontend/src/assets/fileTree.css: -------------------------------------------------------------------------------- 1 | .files-wrapper { 2 | max-width:29%; 3 | float:left; 4 | border: 1px solid #ddd; 5 | border-radius: 3px; 6 | margin-bottom: 1em; 7 | margin-right:5px; 8 | } 9 | 10 | .navigation-tree { 11 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 12 | font-size: 15px; 13 | } 14 | 15 | .navigation-tree ul { 16 | list-style: none; 17 | padding: 0; 18 | } 19 | 20 | .tree-root { 21 | padding: 5px; 22 | overflow-x:auto; 23 | } 24 | 25 | .tree-wrapper { 26 | padding: 15px 5px; 27 | } 28 | 29 | .tree .file { 30 | white-space: nowrap; 31 | margin-left: 5px; 32 | } 33 | 34 | .tree .file.selected { 35 | text-decoration: underline; 36 | } 37 | 38 | .tree .file i { 39 | margin-right: 5px; 40 | } 41 | 42 | .tree .tree { 43 | padding-left:10px; 44 | } 45 | 46 | .tree .files { 47 | padding-left:10px; 48 | } 49 | 50 | .tree .dir-name { 51 | font-weight: bold; 52 | padding-left: 5px; 53 | } 54 | 55 | .tree a { 56 | text-decoration: none; 57 | } 58 | 59 | .directory-hidden .tree, 60 | .directory-hidden .files { 61 | display: none; 62 | 63 | } 64 | 65 | -------------------------------------------------------------------------------- /frontend/src/assets/img/diffy-logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/assets/img/diffy-logo.gif -------------------------------------------------------------------------------- /frontend/src/assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/assets/img/favicon.ico -------------------------------------------------------------------------------- /frontend/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbu88/diffy/bd5cde50264bb245ffaa874ce6129adf70839f23/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Diffy - share diff output in your browser 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 | 43 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {AppModule} from './app/app.module'; 5 | import {environment} from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule).catch(err => console.error(err)); 12 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** 38 | * If the application will be indexed by Google Search, the following is required. 39 | * Googlebot uses a renderer based on Chrome 41. 40 | * https://developers.google.com/search/docs/guides/rendering 41 | **/ 42 | // import 'core-js/es6/array'; 43 | 44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 45 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 46 | 47 | /** IE10 and IE11 requires the following for the Reflect API. */ 48 | // import 'core-js/es6/reflect'; 49 | 50 | /** 51 | * Web Animations `@angular/platform-browser/animations` 52 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 53 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 54 | **/ 55 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 56 | 57 | /** 58 | * By default, zone.js will patch all possible macroTask and DomEvents 59 | * user can disable parts of macroTask/DomEvents patch by setting following flags 60 | */ 61 | 62 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch 63 | // requestAnimationFrame (window as any).__Zone_disable_on_property = true; // disable patch 64 | // onProperty such as onclick (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 65 | // 'mousemove']; // disable patch specified eventNames 66 | 67 | /* 68 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 69 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 70 | */ 71 | // (window as any).__Zone_enable_cross_context_check = true; 72 | 73 | /*************************************************************************************************** 74 | * Zone JS is required by default for Angular itself. 75 | */ 76 | import 'zone.js/dist/zone'; // Included with Angular CLI. 77 | 78 | 79 | /*************************************************************************************************** 80 | * APPLICATION IMPORTS 81 | */ 82 | 83 | (window as any).global = window; 84 | (window as any).process = { 85 | env: {DEBUG: undefined}, 86 | }; 87 | -------------------------------------------------------------------------------- /frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | .btn-file { 3 | position: relative; 4 | overflow: hidden; 5 | } 6 | 7 | .btn-file input[type=file] { 8 | position: absolute; 9 | top: 0; 10 | right: 0; 11 | min-width: 100%; 12 | min-height: 100%; 13 | font-size: 100px; 14 | text-align: right; 15 | filter: alpha(opacity=0); 16 | opacity: 0; 17 | outline: none; 18 | background: white; 19 | cursor: inherit; 20 | display: block; 21 | } 22 | 23 | html, 24 | body { 25 | height: 100%; 26 | /* The html and body elements cannot have any padding or margin. */ 27 | } 28 | 29 | /* Wrapper for page content to push down footer */ 30 | #wrap { 31 | min-height: 100%; 32 | height: auto !important; 33 | height: 100%; 34 | } 35 | #push, 36 | #footer { 37 | height: 60px; 38 | } 39 | #footer { 40 | background-color: #f5f5f5; 41 | } 42 | 43 | /* Lastly, apply responsive CSS fixes as necessary */ 44 | @media (max-width: 767px) { 45 | #footer { 46 | margin-left: -20px; 47 | margin-right: -20px; 48 | padding-left: 20px; 49 | padding-right: 20px; 50 | } 51 | } 52 | 53 | .container .credit { 54 | margin: 20px 0; 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import {getTestBed} from '@angular/core/testing'; 5 | import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; 6 | 7 | declare const require: any; 8 | 9 | // First, initialize the Angular testing environment. 10 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 11 | // Then we find all the tests. 12 | const context = require.context('./', true, /\.spec\.ts$/); 13 | // And load the modules. 14 | context.keys().map(context); 15 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "paths": { 17 | "diff2html": ["../node_modules/diff2html/dist/diff2html.js"] 18 | }, 19 | "lib": [ 20 | "es2018", 21 | "dom" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /models/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diffy-models", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "diffy-models", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "diff2html": "3.4.9" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "16.7.1", 16 | "typescript": "4.3.5" 17 | } 18 | }, 19 | "node_modules/@types/node": { 20 | "version": "16.7.1", 21 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.1.tgz", 22 | "integrity": "sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A==", 23 | "dev": true 24 | }, 25 | "node_modules/abbrev": { 26 | "version": "1.1.1", 27 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 28 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 29 | }, 30 | "node_modules/diff": { 31 | "version": "5.0.0", 32 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 33 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 34 | "engines": { 35 | "node": ">=0.3.1" 36 | } 37 | }, 38 | "node_modules/diff2html": { 39 | "version": "3.4.9", 40 | "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.9.tgz", 41 | "integrity": "sha512-33x45h6Xgfasjt49e0ldfLnUdCjLjHIdablpAlrKnQyyG1RA7w+4cbp9+bUfNLxfFj584BookXqh5KJEt4+MLA==", 42 | "dependencies": { 43 | "diff": "5.0.0", 44 | "hogan.js": "3.0.2" 45 | }, 46 | "engines": { 47 | "node": ">=12" 48 | }, 49 | "optionalDependencies": { 50 | "highlight.js": "11.1.0" 51 | } 52 | }, 53 | "node_modules/highlight.js": { 54 | "version": "11.1.0", 55 | "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.1.0.tgz", 56 | "integrity": "sha512-X9VVhYKHQPPuwffO8jk4bP/FVj+ibNCy3HxZZNDXFtJrq4O5FdcdCDRIkDis5MiMnjh7UwEdHgRZJcHFYdzDdA==", 57 | "optional": true, 58 | "engines": { 59 | "node": ">=12.0.0" 60 | } 61 | }, 62 | "node_modules/hogan.js": { 63 | "version": "3.0.2", 64 | "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", 65 | "integrity": "sha1-TNnhq9QpQUbnZ55B14mHMrAse/0=", 66 | "dependencies": { 67 | "mkdirp": "0.3.0", 68 | "nopt": "1.0.10" 69 | }, 70 | "bin": { 71 | "hulk": "bin/hulk" 72 | } 73 | }, 74 | "node_modules/mkdirp": { 75 | "version": "0.3.0", 76 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", 77 | "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", 78 | "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", 79 | "engines": { 80 | "node": "*" 81 | } 82 | }, 83 | "node_modules/nopt": { 84 | "version": "1.0.10", 85 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 86 | "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", 87 | "dependencies": { 88 | "abbrev": "1" 89 | }, 90 | "bin": { 91 | "nopt": "bin/nopt.js" 92 | }, 93 | "engines": { 94 | "node": "*" 95 | } 96 | }, 97 | "node_modules/typescript": { 98 | "version": "4.3.5", 99 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", 100 | "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", 101 | "dev": true, 102 | "bin": { 103 | "tsc": "bin/tsc", 104 | "tsserver": "bin/tsserver" 105 | }, 106 | "engines": { 107 | "node": ">=4.2.0" 108 | } 109 | } 110 | }, 111 | "dependencies": { 112 | "@types/node": { 113 | "version": "16.7.1", 114 | "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.1.tgz", 115 | "integrity": "sha512-ncRdc45SoYJ2H4eWU9ReDfp3vtFqDYhjOsKlFFUDEn8V1Bgr2RjYal8YT5byfadWIRluhPFU6JiDOl0H6Sl87A==", 116 | "dev": true 117 | }, 118 | "abbrev": { 119 | "version": "1.1.1", 120 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 121 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 122 | }, 123 | "diff": { 124 | "version": "5.0.0", 125 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 126 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" 127 | }, 128 | "diff2html": { 129 | "version": "3.4.9", 130 | "resolved": "https://registry.npmjs.org/diff2html/-/diff2html-3.4.9.tgz", 131 | "integrity": "sha512-33x45h6Xgfasjt49e0ldfLnUdCjLjHIdablpAlrKnQyyG1RA7w+4cbp9+bUfNLxfFj584BookXqh5KJEt4+MLA==", 132 | "requires": { 133 | "diff": "5.0.0", 134 | "highlight.js": "11.1.0", 135 | "hogan.js": "3.0.2" 136 | } 137 | }, 138 | "highlight.js": { 139 | "version": "11.1.0", 140 | "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.1.0.tgz", 141 | "integrity": "sha512-X9VVhYKHQPPuwffO8jk4bP/FVj+ibNCy3HxZZNDXFtJrq4O5FdcdCDRIkDis5MiMnjh7UwEdHgRZJcHFYdzDdA==", 142 | "optional": true 143 | }, 144 | "hogan.js": { 145 | "version": "3.0.2", 146 | "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", 147 | "integrity": "sha1-TNnhq9QpQUbnZ55B14mHMrAse/0=", 148 | "requires": { 149 | "mkdirp": "0.3.0", 150 | "nopt": "1.0.10" 151 | } 152 | }, 153 | "mkdirp": { 154 | "version": "0.3.0", 155 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", 156 | "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=" 157 | }, 158 | "nopt": { 159 | "version": "1.0.10", 160 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", 161 | "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", 162 | "requires": { 163 | "abbrev": "1" 164 | } 165 | }, 166 | "typescript": { 167 | "version": "4.3.5", 168 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", 169 | "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", 170 | "dev": true 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /models/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diffy-models", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "rm -r dist;tsc" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "diff2html": "3.4.9" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "16.7.1", 17 | "typescript": "4.3.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /models/src/ActionDefinitions.ts: -------------------------------------------------------------------------------- 1 | export abstract class InputParser { 2 | public abstract parse (req: any): Input; 3 | } 4 | export abstract class Input {} 5 | 6 | 7 | export class ContextParser { 8 | public parse (req: any): Context { 9 | return { 10 | gaCookie: this.readOrUndefined(() => { 11 | if(req.cookies._ga == undefined) { 12 | console.info("Undefined cookies._ga"); 13 | return undefined; 14 | } 15 | const rawGa: string = req.cookies._ga; 16 | const parts = rawGa.split('.') 17 | if(parts.length == 4) { 18 | return parts[2] + "." + parts[3]; 19 | } 20 | console.info("Strange cookies._ga format", rawGa); 21 | return rawGa; 22 | }), 23 | } 24 | } 25 | 26 | private readOrUndefined(getterFn: () => string): string | undefined { 27 | try { 28 | return getterFn() 29 | } catch { 30 | return undefined; 31 | } 32 | } 33 | } 34 | 35 | export interface Context { 36 | gaCookie: string | undefined; 37 | } 38 | 39 | export abstract class Output { 40 | public abstract serialize(): string; 41 | } 42 | 43 | export abstract class ActionFactory { 44 | public abstract create(): ActionPromise; 45 | } 46 | 47 | export abstract class Action { 48 | public abstract execute(input: I, context: C): O; 49 | } 50 | 51 | export abstract class ActionPromise { 52 | public abstract execute(input: I, context: C): Promise; 53 | } -------------------------------------------------------------------------------- /models/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./models"; 2 | export * from "./ActionDefinitions"; 3 | export * from "./io/GetDiff"; 4 | export * from "./io/CreateDiff"; 5 | export * from "./io/DeleteDiff"; 6 | export * from "./io/ExtendDiffLifetime"; 7 | export * from "./io/MakeDiffPermanent"; -------------------------------------------------------------------------------- /models/src/io/CreateDiff.ts: -------------------------------------------------------------------------------- 1 | import { SharedDiff } from "../models"; 2 | import { Input, InputParser, Output } from "../ActionDefinitions"; 3 | 4 | export class CreateDiffInputFactory extends InputParser { 5 | 6 | public parse(req: any): CreateDiffInput { 7 | let diffRequest = req.body; 8 | let diff: string = diffRequest.diff || ''; 9 | diff = diff.replace(/\r/g, ''); 10 | return { 11 | diff, 12 | } 13 | } 14 | } 15 | 16 | export class CreateDiffInput implements Input { 17 | diff: string; 18 | } 19 | 20 | export class CreateDiffOutput extends Output { 21 | private _sharedDiff: SharedDiff; 22 | 23 | constructor(sharedDiff: SharedDiff) { 24 | super(); 25 | this._sharedDiff = sharedDiff; 26 | 27 | } 28 | 29 | public get sharedDiff(): SharedDiff { 30 | return this._sharedDiff; 31 | } 32 | 33 | public serialize(): string { 34 | return JSON.stringify(this.sharedDiff); 35 | } 36 | } -------------------------------------------------------------------------------- /models/src/io/DeleteDiff.ts: -------------------------------------------------------------------------------- 1 | import { Input, InputParser, Output } from "../ActionDefinitions"; 2 | 3 | export class DeleteDiffInputFactory extends InputParser { 4 | 5 | public parse(req: any): DeleteDiffInput { 6 | return { 7 | id: req.params.id, 8 | } 9 | } 10 | } 11 | 12 | export class DeleteDiffInput implements Input { 13 | id: string; 14 | } 15 | 16 | export class DeleteDiffOutput extends Output { 17 | private _success: boolean; 18 | 19 | constructor(success: boolean) { 20 | super(); 21 | this._success = success; 22 | 23 | } 24 | 25 | public get success(): boolean { 26 | return this._success; 27 | } 28 | 29 | public serialize(): string { 30 | return JSON.stringify(this._success); 31 | } 32 | } -------------------------------------------------------------------------------- /models/src/io/ExtendDiffLifetime.ts: -------------------------------------------------------------------------------- 1 | import { SharedDiff } from "../models"; 2 | import { Input, InputParser, Output } from "../ActionDefinitions"; 3 | 4 | export class ExtendDiffLifetimeInputFactory extends InputParser { 5 | 6 | public parse(req: any): ExtendDiffLifetimeInput { 7 | return { 8 | id: req.params.id, 9 | } 10 | } 11 | } 12 | 13 | export class ExtendDiffLifetimeInput implements Input { 14 | id: string; 15 | } 16 | 17 | export class ExtendDiffLifetimeOutput extends Output { 18 | private _sharedDiff: SharedDiff; 19 | 20 | constructor(sharedDiff: SharedDiff) { 21 | super(); 22 | this._sharedDiff = sharedDiff; 23 | 24 | } 25 | 26 | public get sharedDiff(): SharedDiff { 27 | return this._sharedDiff; 28 | } 29 | 30 | public serialize(): string { 31 | return JSON.stringify(this.sharedDiff); 32 | } 33 | } -------------------------------------------------------------------------------- /models/src/io/GetDiff.ts: -------------------------------------------------------------------------------- 1 | import { SharedDiff } from "../models"; 2 | import { Input, InputParser, Output } from "../ActionDefinitions"; 3 | 4 | export class GetDiffInputFactory extends InputParser { 5 | 6 | public parse(req: any): GetDiffInput { 7 | return { 8 | id: req.params.id, 9 | } 10 | } 11 | } 12 | 13 | export class GetDiffInput implements Input { 14 | id: string; 15 | } 16 | 17 | export class GetDiffOutput extends Output { 18 | private _sharedDiff: SharedDiff; 19 | 20 | constructor(sharedDiff: SharedDiff) { 21 | super(); 22 | this._sharedDiff = sharedDiff; 23 | 24 | } 25 | 26 | public get sharedDiff(): SharedDiff { 27 | return this._sharedDiff; 28 | } 29 | 30 | public serialize(): string { 31 | return JSON.stringify(this.sharedDiff); 32 | } 33 | } -------------------------------------------------------------------------------- /models/src/io/MakeDiffPermanent.ts: -------------------------------------------------------------------------------- 1 | import { SharedDiff } from "../models"; 2 | import { Input, InputParser, Output } from "../ActionDefinitions"; 3 | 4 | export class MakeDiffPermanentInputFactory extends InputParser { 5 | 6 | public parse(req: any): MakeDiffPermanentInput { 7 | return { 8 | id: req.params.id, 9 | } 10 | } 11 | } 12 | 13 | export class MakeDiffPermanentInput implements Input { 14 | id: string; 15 | } 16 | 17 | export class MakeDiffPermanentOutput extends Output { 18 | private _sharedDiff: SharedDiff; 19 | 20 | constructor(sharedDiff: SharedDiff) { 21 | super(); 22 | this._sharedDiff = sharedDiff; 23 | 24 | } 25 | 26 | public get sharedDiff(): SharedDiff { 27 | return this._sharedDiff; 28 | } 29 | 30 | public serialize(): string { 31 | return JSON.stringify(this.sharedDiff); 32 | } 33 | } -------------------------------------------------------------------------------- /models/src/models.ts: -------------------------------------------------------------------------------- 1 | import { DiffFile } from 'diff2html/lib/types'; 2 | 3 | export interface DiffyInput { 4 | rawDiff: string; 5 | } 6 | 7 | export interface SharedDiff { 8 | id?: string, 9 | created: Date, 10 | expiresAt: Date, 11 | diff: DiffFile[], 12 | rawDiff: string, 13 | }; -------------------------------------------------------------------------------- /models/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "commonjs", 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "allowJs": true, 12 | "rootDir": "src", 13 | }, 14 | "include": [ 15 | "src" 16 | ], 17 | "exclude": ["node_modules", "dist"] 18 | } 19 | -------------------------------------------------------------------------------- /tools/backend_compile.sh: -------------------------------------------------------------------------------- 1 | docker-compose run -v "$(pwd):/diffy/" -w '/diffy/backend' --rm web npm run-script build 2 | -------------------------------------------------------------------------------- /tools/backend_install.sh: -------------------------------------------------------------------------------- 1 | docker-compose run -v "$(pwd):/diffy/" -w '/diffy/backend' --rm web npm install 2 | -------------------------------------------------------------------------------- /tools/format_code.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run where the Dockerfile lives (root of the project) 3 | set -x 4 | 5 | docker run -v "$(pwd):/diffy/" -w '/diffy' --rm diffy clang-format -i --glob='**/*.ts' 6 | -------------------------------------------------------------------------------- /tools/frontend_compile.sh: -------------------------------------------------------------------------------- 1 | docker-compose run -v "$(pwd):/diffy/" -w '/diffy/frontend' --rm web npm run-script build 2 | -------------------------------------------------------------------------------- /tools/frontend_install.sh: -------------------------------------------------------------------------------- 1 | docker-compose run web /bin/sh -c 'cd ../frontend; npm install' 2 | -------------------------------------------------------------------------------- /tools/models_compile.sh: -------------------------------------------------------------------------------- 1 | docker-compose run -v "$(pwd):/diffy/" -w '/diffy/models' --rm web npm run-script build 2 | -------------------------------------------------------------------------------- /tools/ng_build_watch.sh: -------------------------------------------------------------------------------- 1 | docker-compose run --rm --no-deps web /bin/sh -c 'cd ../frontend; ng build --watch' 2 | -------------------------------------------------------------------------------- /tools/rebuild_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | 4 | rm -r backend/node_modules 5 | rm -r frontend/node_modules 6 | docker build --no-cache --rm -t diffy . 7 | -------------------------------------------------------------------------------- /tools/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker-compose run -v "$(pwd):/diffy/" -w '/diffy/backend' --rm web npm test 3 | --------------------------------------------------------------------------------