From 52213e6ad6a9de5007d1d0504384f91ebdb748f8 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 17 Oct 2021 20:58:48 +0100 Subject: [PATCH 1/9] Break out git diff from Git.ts --- packages/git/src/Git.test.ts | 408 +---------------- packages/git/src/Git.ts | 123 +---- packages/git/src/GitDiff.test.ts | 422 ++++++++++++++++++ packages/git/src/GitDiff.ts | 127 ++++++ .../renderer/components/diff/useDiffData.ts | 4 +- 5 files changed, 557 insertions(+), 527 deletions(-) create mode 100644 packages/git/src/GitDiff.test.ts create mode 100644 packages/git/src/GitDiff.ts diff --git a/packages/git/src/Git.test.ts b/packages/git/src/Git.test.ts index c670a662..42e2cea0 100644 --- a/packages/git/src/Git.test.ts +++ b/packages/git/src/Git.test.ts @@ -1,6 +1,6 @@ import { Git } from './Git' import { STATE } from './constants' -import { DiffResult, DiffFile, StatusFile } from './types' +import { StatusFile } from './types' import { TestGitShim } from './TestGitShim' @@ -451,410 +451,4 @@ describe('Git', () => { ) }) }) - - describe('getDiffFromShas', () => { - async function getBaseCommit(): Promise { - await spawn(['init']) - shim.writeFile('onlyfile', 'abc') - return await shim.commit('Base Commit') - } - - describe.each([ - ['one commit', false], - ['two commits', true], - ])('fetching diff for %s', (caseName: string, useOldSha: boolean) => { - it('added file', async () => { - const baseSha = await getBaseCommit() - const git = new Git(dir) - - shim.writeFile('a.txt', 'line 1\nline 2') - const sha = await shim.commit('Commit 1') - - const diff = (await git.getDiffFromShas( - sha, - useOldSha ? baseSha : null, - ))! - - expect(diff).toEqual({ - stats: { - deletions: 0, - filesChanged: 1, - insertions: 2, - }, - files: [ - expect.objectContaining>({ - oldName: null, - newName: 'a.txt', - addedLines: 2, - deletedLines: 0, - isNew: true, - isRename: false, - isDeleted: false, - isModified: false, - }), - ], - }) - }) - - it('renamed file', async () => { - await getBaseCommit() - const git = new Git(dir) - - shim.writeFile('a.txt', 'line 1\nline 2') - const baseSha = await shim.commit('Commit 1') - shim.renameFile('a.txt', 'b.txt') - const sha = await shim.commit('Commit 2') - - const diff = (await git.getDiffFromShas( - sha, - useOldSha ? baseSha : null, - ))! - - expect(diff).toEqual({ - stats: { - deletions: 0, - filesChanged: 1, - insertions: 0, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: 'b.txt', - addedLines: 0, - isNew: false, - isRename: true, - isModified: true, - isDeleted: false, - }), - ], - }) - }) - - it('modified file', async () => { - await getBaseCommit() - const git = new Git(dir) - - shim.writeFile('a.txt', 'line 1\nline 3') - const baseSha = await shim.commit('Commit 1') - shim.writeFile('a.txt', 'line 1\nline 2') - const sha = await shim.commit('Commit 2') - - const diff = (await git.getDiffFromShas( - sha, - useOldSha ? baseSha : null, - ))! - - expect(diff).toEqual({ - stats: { - deletions: 1, - filesChanged: 1, - insertions: 1, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: 'a.txt', - addedLines: 1, - deletedLines: 1, - isNew: false, - isRename: false, - isModified: true, - isDeleted: false, - }), - ], - }) - }) - - it('deleted file', async () => { - await getBaseCommit() - const git = new Git(dir) - - shim.writeFile('a.txt', 'line 1\nline 2') - const baseSha = await shim.commit('Commit 1') - shim.rmFile('a.txt') - const sha = await shim.commit('Commit 2') - - const diff = (await git.getDiffFromShas( - sha, - useOldSha ? baseSha : null, - ))! - - expect(diff).toEqual({ - stats: { - deletions: 2, - filesChanged: 1, - insertions: 0, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: null, - addedLines: 0, - deletedLines: 2, - isNew: false, - isRename: false, - isModified: false, - isDeleted: true, - }), - ], - }) - }) - }) - }) - - describe('getDiffFromIndex', () => { - async function getBaseCommit(): Promise { - await spawn(['init']) - shim.writeFile('a.txt', 'line 1\nline 2') - return await shim.commit('Base Commit') - } - - it.each<('staged' | 'unstaged')[]>([['staged'], ['unstaged']])( - 'added file (%s)', - async (setup: 'staged' | 'unstaged') => { - await getBaseCommit() - const git = new Git(dir) - - shim.writeFile('b.txt', 'line 1\nline 2') - await spawn(['add', '--all']) - if (setup === 'staged') { - await spawn(['add', '--all']) - } - - const diff = (await git.getDiffFromIndex())! - - expect(diff).toEqual({ - stats: { - deletions: 0, - filesChanged: 1, - insertions: 2, - }, - files: [ - expect.objectContaining>({ - oldName: null, - newName: 'b.txt', - addedLines: 2, - deletedLines: 0, - isNew: true, - isRename: false, - isDeleted: false, - isModified: false, - }), - ], - }) - }, - ) - - it.each<('staged' | 'unstaged')[]>([['staged'], ['unstaged']])( - 'modified file (%s)', - async (setup: 'staged' | 'unstaged') => { - await getBaseCommit() - const git = new Git(dir) - - shim.writeFile('a.txt', 'line 1\nline 3') - await shim.commit('Commit 1') - shim.writeFile('a.txt', 'line 1\nline 2') - if (setup === 'staged') { - await spawn(['add', '--all']) - } - - const diff = (await git.getDiffFromIndex())! - - expect(diff).toEqual({ - stats: { - deletions: 1, - filesChanged: 1, - insertions: 1, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: 'a.txt', - addedLines: 1, - deletedLines: 1, - isNew: false, - isRename: false, - isModified: true, - isDeleted: false, - }), - ], - }) - }, - ) - - it.each<('staged' | 'unstaged')[]>([['staged'], ['unstaged']])( - 'deleted file (%s)', - async (setup: 'staged' | 'unstaged') => { - await getBaseCommit() - const git = new Git(dir) - - shim.rmFile('a.txt') - if (setup === 'staged') { - await spawn(['add', '--all']) - } - - const diff = (await git.getDiffFromIndex())! - - expect(diff).toEqual({ - stats: { - deletions: 2, - filesChanged: 1, - insertions: 0, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: null, - addedLines: 0, - deletedLines: 2, - isNew: false, - isRename: false, - isModified: false, - isDeleted: true, - }), - ], - }) - }, - ) - - it('renamed file (unstaged)', async () => { - await getBaseCommit() - const git = new Git(dir) - - shim.renameFile('a.txt', 'b.txt') - - const diff = (await git.getDiffFromIndex())! - - expect(diff).toEqual({ - stats: { - deletions: 2, - filesChanged: 2, - insertions: 2, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: null, - addedLines: 0, - deletedLines: 2, - isNew: false, - isRename: false, - isModified: false, - isDeleted: true, - }), - expect.objectContaining>({ - oldName: null, - newName: 'b.txt', - addedLines: 2, - deletedLines: 0, - isNew: true, - isRename: false, - isModified: false, - isDeleted: false, - }), - ], - }) - }) - - it('renamed and modified file (unstaged)', async () => { - await getBaseCommit() - const git = new Git(dir) - - shim.renameFile('a.txt', 'b.txt') - shim.writeFile('b.txt', 'line 1\nline 3') - - const diff = (await git.getDiffFromIndex())! - - expect(diff).toEqual({ - stats: { - deletions: 2, - filesChanged: 2, - insertions: 2, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: null, - addedLines: 0, - deletedLines: 2, - isNew: false, - isRename: false, - isModified: false, - isDeleted: true, - }), - expect.objectContaining>({ - oldName: null, - newName: 'b.txt', - addedLines: 2, - deletedLines: 0, - isNew: true, - isRename: false, - isModified: false, - isDeleted: false, - }), - ], - }) - }) - - it('renamed file (staged)', async () => { - await getBaseCommit() - const git = new Git(dir) - - shim.renameFile('a.txt', 'b.txt') - await spawn(['add', '--all']) - - const diff = (await git.getDiffFromIndex())! - - expect(diff).toEqual({ - stats: { - deletions: 0, - filesChanged: 1, - insertions: 0, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: 'b.txt', - addedLines: 0, - deletedLines: 0, - isNew: false, - isRename: true, - isModified: true, - isDeleted: false, - }), - ], - }) - }) - - it('renamed and modified file (staged)', async () => { - await getBaseCommit() - const git = new Git(dir) - - shim.renameFile('a.txt', 'b.txt') - shim.writeFile('b.txt', 'line 1\nline 3') - await spawn(['add', '--all']) - - const diff = (await git.getDiffFromIndex())! - - expect(diff).toEqual({ - stats: { - deletions: 1, - filesChanged: 1, - insertions: 1, - }, - files: [ - expect.objectContaining>({ - oldName: 'a.txt', - newName: 'b.txt', - addedLines: 1, - deletedLines: 1, - isNew: false, - isRename: true, - isModified: true, - isDeleted: false, - }), - ], - }) - }) - }) }) diff --git a/packages/git/src/Git.ts b/packages/git/src/Git.ts index 430f69fa..5cacd595 100644 --- a/packages/git/src/Git.ts +++ b/packages/git/src/Git.ts @@ -1,5 +1,4 @@ import _ from 'lodash' -import * as Diff2Html from 'diff2html' import chokidar from 'chokidar' import path from 'path' @@ -9,18 +8,11 @@ import { spawn } from 'child_process' import { resolveRepo } from './resolve-repo' import { STATE, STATE_FILES } from './constants' -import type { - DiffFile, - DiffResult, - GitFileOp, - GetSpawn, - StatusFile, - FileText, - Remote, -} from './types' +import type { GitFileOp, GetSpawn, StatusFile, FileText, Remote } from './types' import { GitRefs } from './GitRefs' import { GitCommits } from './GitCommits' +import { GitDiff } from './GitDiff' import { Watcher } from './Watcher' import { perfStart, instrumentClass } from './performance' @@ -31,6 +23,7 @@ export class Git { readonly refs: GitRefs readonly commits: GitCommits + readonly diff: GitDiff readonly watcher: Watcher constructor(cwd: string) { @@ -39,11 +32,13 @@ export class Git { this.refs = new GitRefs(this.cwd, this._getSpawn) this.commits = new GitCommits(this.cwd, this._getSpawn, this) + this.diff = new GitDiff(this.cwd, this._getSpawn, this) this.watcher = new Watcher(this.cwd) instrumentClass(this) instrumentClass(this.refs) instrumentClass(this.commits) + instrumentClass(this.diff) } _getGitDir = async () => { @@ -312,112 +307,4 @@ export class Git { type: fileType, } } - - getDiffFromShas = async ( - shaNew: string, - shaOld: string | null = null, - { contextLines = 10 } = {}, - ): Promise => { - const spawn = await this._getSpawn() - if (!spawn) { - return null - } - - // From Git: - // git diff SHAOLD SHANEW --unified=10 - // git show SHA --patch -m - - if (!shaNew) { - console.error('shaNew was not provided') - return null - } - - let cmd = [] - if (shaOld) { - cmd = ['diff', shaOld, shaNew, '--unified=' + contextLines] - } else { - cmd = [ - 'show', - shaNew, - '--patch', // Always show patch - '-m', // Show patch even on merge commits - '--unified=' + contextLines, - ] - } - - const patchText = await spawn(cmd) - const diff = await this.processDiff(patchText) - - return diff - } - - getDiffFromIndex = async ({ - contextLines = 5, - } = {}): Promise => { - const spawn = await this._getSpawn() - if (!spawn) { - return null - } - - const statusFiles = await this.getStatus() - - const diffTexts = await Promise.all( - statusFiles.map(async (statusFile) => { - const cmd = ['diff', '--unified=' + contextLines] - - if (statusFile.isNew) { - // Has to be compared to an empty file - cmd.push('/dev/null', statusFile.path) - } else if (statusFile.isDeleted) { - // Has to be compared to current HEAD tree - cmd.push('HEAD', '--', statusFile.path) - } else if (statusFile.isModified) { - // Compare back to head - cmd.push('HEAD', statusFile.path) - } else if (statusFile.isRenamed) { - // Have to tell git diff about the rename - cmd.push('HEAD', '--', statusFile.oldPath!, statusFile.path) - } - - return await spawn(cmd, { okCodes: [0, 1] }) - }), - ) - - const diffText = diffTexts.join('\n') - - const diff = await this.processDiff(diffText) - - return diff - } - - private processDiff = async (diffText: string): Promise => { - const files = Diff2Html.parse(diffText) as DiffFile[] - - for (const file of files) { - if (file.oldName === '/dev/null') { - file.oldName = null - } - if (file.newName === '/dev/null') { - file.newName = null - } - - // Diff2Html doesn't attach false values, so patch these on - file.isNew = !!file.isNew - file.isDeleted = !!file.isDeleted - file.isRename = !!file.isRename - file.isModified = !file.isNew && !file.isDeleted - } - - return { - stats: { - insertions: files.reduce((result, file) => file.addedLines + result, 0), - filesChanged: files.length, - deletions: files.reduce( - (result, file) => file.deletedLines + result, - 0, - ), - }, - files, - } - } } diff --git a/packages/git/src/GitDiff.test.ts b/packages/git/src/GitDiff.test.ts new file mode 100644 index 00000000..5c5d3b78 --- /dev/null +++ b/packages/git/src/GitDiff.test.ts @@ -0,0 +1,422 @@ +import { Git } from './Git' +import { DiffResult, DiffFile } from './types' + +import { TestGitShim } from './TestGitShim' + +describe('Git', () => { + let shim: TestGitShim + let dir = '' + let spawn: ReturnType + + beforeEach(async () => { + shim = new TestGitShim() + dir = shim.dir + spawn = shim.getSpawn(dir) + }) + + describe('getDiffFromShas', () => { + async function getBaseCommit(): Promise { + await spawn(['init']) + shim.writeFile('onlyfile', 'abc') + return await shim.commit('Base Commit') + } + + describe.each([ + ['one commit', false], + ['two commits', true], + ])('fetching diff for %s', (caseName: string, useOldSha: boolean) => { + it('added file', async () => { + const baseSha = await getBaseCommit() + const git = new Git(dir) + + shim.writeFile('a.txt', 'line 1\nline 2') + const sha = await shim.commit('Commit 1') + + const diff = (await git.diff.getDiffFromShas( + sha, + useOldSha ? baseSha : null, + ))! + + expect(diff).toEqual({ + stats: { + deletions: 0, + filesChanged: 1, + insertions: 2, + }, + files: [ + expect.objectContaining>({ + oldName: null, + newName: 'a.txt', + addedLines: 2, + deletedLines: 0, + isNew: true, + isRename: false, + isDeleted: false, + isModified: false, + }), + ], + }) + }) + + it('renamed file', async () => { + await getBaseCommit() + const git = new Git(dir) + + shim.writeFile('a.txt', 'line 1\nline 2') + const baseSha = await shim.commit('Commit 1') + shim.renameFile('a.txt', 'b.txt') + const sha = await shim.commit('Commit 2') + + const diff = (await git.diff.getDiffFromShas( + sha, + useOldSha ? baseSha : null, + ))! + + expect(diff).toEqual({ + stats: { + deletions: 0, + filesChanged: 1, + insertions: 0, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: 'b.txt', + addedLines: 0, + isNew: false, + isRename: true, + isModified: true, + isDeleted: false, + }), + ], + }) + }) + + it('modified file', async () => { + await getBaseCommit() + const git = new Git(dir) + + shim.writeFile('a.txt', 'line 1\nline 3') + const baseSha = await shim.commit('Commit 1') + shim.writeFile('a.txt', 'line 1\nline 2') + const sha = await shim.commit('Commit 2') + + const diff = (await git.diff.getDiffFromShas( + sha, + useOldSha ? baseSha : null, + ))! + + expect(diff).toEqual({ + stats: { + deletions: 1, + filesChanged: 1, + insertions: 1, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: 'a.txt', + addedLines: 1, + deletedLines: 1, + isNew: false, + isRename: false, + isModified: true, + isDeleted: false, + }), + ], + }) + }) + + it('deleted file', async () => { + await getBaseCommit() + const git = new Git(dir) + + shim.writeFile('a.txt', 'line 1\nline 2') + const baseSha = await shim.commit('Commit 1') + shim.rmFile('a.txt') + const sha = await shim.commit('Commit 2') + + const diff = (await git.diff.getDiffFromShas( + sha, + useOldSha ? baseSha : null, + ))! + + expect(diff).toEqual({ + stats: { + deletions: 2, + filesChanged: 1, + insertions: 0, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: null, + addedLines: 0, + deletedLines: 2, + isNew: false, + isRename: false, + isModified: false, + isDeleted: true, + }), + ], + }) + }) + }) + }) + + describe('getDiffFromIndex', () => { + async function getBaseCommit(): Promise { + await spawn(['init']) + shim.writeFile('a.txt', 'line 1\nline 2') + return await shim.commit('Base Commit') + } + + it.each<('staged' | 'unstaged')[]>([['staged'], ['unstaged']])( + 'added file (%s)', + async (setup: 'staged' | 'unstaged') => { + await getBaseCommit() + const git = new Git(dir) + + shim.writeFile('b.txt', 'line 1\nline 2') + await spawn(['add', '--all']) + if (setup === 'staged') { + await spawn(['add', '--all']) + } + + const diff = (await git.diff.getDiffFromIndex())! + + expect(diff).toEqual({ + stats: { + deletions: 0, + filesChanged: 1, + insertions: 2, + }, + files: [ + expect.objectContaining>({ + oldName: null, + newName: 'b.txt', + addedLines: 2, + deletedLines: 0, + isNew: true, + isRename: false, + isDeleted: false, + isModified: false, + }), + ], + }) + }, + ) + + it.each<('staged' | 'unstaged')[]>([['staged'], ['unstaged']])( + 'modified file (%s)', + async (setup: 'staged' | 'unstaged') => { + await getBaseCommit() + const git = new Git(dir) + + shim.writeFile('a.txt', 'line 1\nline 3') + await shim.commit('Commit 1') + shim.writeFile('a.txt', 'line 1\nline 2') + if (setup === 'staged') { + await spawn(['add', '--all']) + } + + const diff = (await git.diff.getDiffFromIndex())! + + expect(diff).toEqual({ + stats: { + deletions: 1, + filesChanged: 1, + insertions: 1, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: 'a.txt', + addedLines: 1, + deletedLines: 1, + isNew: false, + isRename: false, + isModified: true, + isDeleted: false, + }), + ], + }) + }, + ) + + it.each<('staged' | 'unstaged')[]>([['staged'], ['unstaged']])( + 'deleted file (%s)', + async (setup: 'staged' | 'unstaged') => { + await getBaseCommit() + const git = new Git(dir) + + shim.rmFile('a.txt') + if (setup === 'staged') { + await spawn(['add', '--all']) + } + + const diff = (await git.diff.getDiffFromIndex())! + + expect(diff).toEqual({ + stats: { + deletions: 2, + filesChanged: 1, + insertions: 0, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: null, + addedLines: 0, + deletedLines: 2, + isNew: false, + isRename: false, + isModified: false, + isDeleted: true, + }), + ], + }) + }, + ) + + it('renamed file (unstaged)', async () => { + await getBaseCommit() + const git = new Git(dir) + + shim.renameFile('a.txt', 'b.txt') + + const diff = (await git.diff.getDiffFromIndex())! + + expect(diff).toEqual({ + stats: { + deletions: 2, + filesChanged: 2, + insertions: 2, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: null, + addedLines: 0, + deletedLines: 2, + isNew: false, + isRename: false, + isModified: false, + isDeleted: true, + }), + expect.objectContaining>({ + oldName: null, + newName: 'b.txt', + addedLines: 2, + deletedLines: 0, + isNew: true, + isRename: false, + isModified: false, + isDeleted: false, + }), + ], + }) + }) + + it('renamed and modified file (unstaged)', async () => { + await getBaseCommit() + const git = new Git(dir) + + shim.renameFile('a.txt', 'b.txt') + shim.writeFile('b.txt', 'line 1\nline 3') + + const diff = (await git.diff.getDiffFromIndex())! + + expect(diff).toEqual({ + stats: { + deletions: 2, + filesChanged: 2, + insertions: 2, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: null, + addedLines: 0, + deletedLines: 2, + isNew: false, + isRename: false, + isModified: false, + isDeleted: true, + }), + expect.objectContaining>({ + oldName: null, + newName: 'b.txt', + addedLines: 2, + deletedLines: 0, + isNew: true, + isRename: false, + isModified: false, + isDeleted: false, + }), + ], + }) + }) + + it('renamed file (staged)', async () => { + await getBaseCommit() + const git = new Git(dir) + + shim.renameFile('a.txt', 'b.txt') + await spawn(['add', '--all']) + + const diff = (await git.diff.getDiffFromIndex())! + + expect(diff).toEqual({ + stats: { + deletions: 0, + filesChanged: 1, + insertions: 0, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: 'b.txt', + addedLines: 0, + deletedLines: 0, + isNew: false, + isRename: true, + isModified: true, + isDeleted: false, + }), + ], + }) + }) + + it('renamed and modified file (staged)', async () => { + await getBaseCommit() + const git = new Git(dir) + + shim.renameFile('a.txt', 'b.txt') + shim.writeFile('b.txt', 'line 1\nline 3') + await spawn(['add', '--all']) + + const diff = (await git.diff.getDiffFromIndex())! + + expect(diff).toEqual({ + stats: { + deletions: 1, + filesChanged: 1, + insertions: 1, + }, + files: [ + expect.objectContaining>({ + oldName: 'a.txt', + newName: 'b.txt', + addedLines: 1, + deletedLines: 1, + isNew: false, + isRename: true, + isModified: true, + isDeleted: false, + }), + ], + }) + }) + }) +}) diff --git a/packages/git/src/GitDiff.ts b/packages/git/src/GitDiff.ts new file mode 100644 index 00000000..1263ca77 --- /dev/null +++ b/packages/git/src/GitDiff.ts @@ -0,0 +1,127 @@ +import _ from 'lodash' +import * as Diff2Html from 'diff2html' + +import { Git } from './Git' + +import type { DiffFile, DiffResult, GetSpawn } from './types' + +export class GitDiff { + _cwd: string + _getSpawn: GetSpawn + private git: Git + + constructor(cwd: string, getSpawn: GetSpawn, git: Git) { + this._cwd = cwd + this._getSpawn = getSpawn + this.git = git + } + + getDiffFromShas = async ( + shaNew: string, + shaOld: string | null = null, + { contextLines = 10 } = {}, + ): Promise => { + const spawn = await this._getSpawn() + if (!spawn) { + return null + } + + // From Git: + // git diff SHAOLD SHANEW --unified=10 + // git show SHA --patch -m + + if (!shaNew) { + console.error('shaNew was not provided') + return null + } + + let cmd = [] + if (shaOld) { + cmd = ['diff', shaOld, shaNew, '--unified=' + contextLines] + } else { + cmd = [ + 'show', + shaNew, + '--patch', // Always show patch + '-m', // Show patch even on merge commits + '--unified=' + contextLines, + ] + } + + const patchText = await spawn(cmd) + const diff = await this.processDiff(patchText) + + return diff + } + + getDiffFromIndex = async ({ + contextLines = 5, + } = {}): Promise => { + const spawn = await this._getSpawn() + if (!spawn) { + return null + } + + // TODO: can we demand this as an input instead to save on 200ms? + const statusFiles = await this.git.getStatus() + + const diffTexts = await Promise.all( + statusFiles.map(async (statusFile) => { + const cmd = ['diff', '--unified=' + contextLines] + + if (statusFile.isNew) { + // Has to be compared to an empty file + cmd.push('/dev/null', statusFile.path) + } else if (statusFile.isDeleted) { + // Has to be compared to current HEAD tree + cmd.push('HEAD', '--', statusFile.path) + } else if (statusFile.isModified) { + // Compare back to head + cmd.push('HEAD', statusFile.path) + } else if (statusFile.isRenamed) { + // Have to tell git diff about the rename + cmd.push('HEAD', '--', statusFile.oldPath!, statusFile.path) + } + + return await spawn(cmd, { okCodes: [0, 1] }) + }), + ) + + const diffText = diffTexts.join('\n') + + const diff = await this.processDiff(diffText) + + return diff + } + + private processDiff = async (diffText: string): Promise => { + const files = Diff2Html.parse(diffText) as DiffFile[] + + for (const file of files) { + if (file.oldName === '/dev/null') { + file.oldName = null + } + if (file.newName === '/dev/null') { + file.newName = null + } + + // Diff2Html doesn't attach false values, so patch these on + file.isNew = !!file.isNew + file.isDeleted = !!file.isDeleted + file.isRename = !!file.isRename + file.isModified = !file.isNew && !file.isDeleted + } + + return { + stats: { + insertions: files.reduce((result, file) => file.addedLines + result, 0), + filesChanged: files.length, + deletions: files.reduce( + (result, file) => file.deletedLines + result, + 0, + ), + }, + files, + } + } +} diff --git a/packages/giterm/app/renderer/components/diff/useDiffData.ts b/packages/giterm/app/renderer/components/diff/useDiffData.ts index 172adb8e..b2d158d1 100644 --- a/packages/giterm/app/renderer/components/diff/useDiffData.ts +++ b/packages/giterm/app/renderer/components/diff/useDiffData.ts @@ -44,10 +44,10 @@ export function useDiffData({ contextLines = 5 } = {}): DiffData { const diff = mode === 'shas' - ? await git.getDiffFromShas(shaNew, shaOld, { + ? await git.diff.getDiffFromShas(shaNew, shaOld, { contextLines, }) - : await git.getDiffFromIndex({ contextLines }) + : await git.diff.getDiffFromIndex({ contextLines }) if (!cancelled) { if (diff) { From 5aefb3a33d2cb04da26b1cff845e976f36c60522 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 17 Oct 2021 21:00:48 +0100 Subject: [PATCH 2/9] Move types out of GitDiff --- packages/git/src/GitDiff.types.ts | 23 +++++++++++++++++++++++ packages/git/src/types.ts | 22 +--------------------- 2 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 packages/git/src/GitDiff.types.ts diff --git a/packages/git/src/GitDiff.types.ts b/packages/git/src/GitDiff.types.ts new file mode 100644 index 00000000..744dfb23 --- /dev/null +++ b/packages/git/src/GitDiff.types.ts @@ -0,0 +1,23 @@ +import * as Diff2Html from 'diff2html/lib-esm/types' + +type Modify = Omit & R + +export interface DiffStats { + insertions: number + deletions: number + filesChanged: number +} + +export type DiffFile = Modify< + Diff2Html.DiffFile, + { + newName: string | null + oldName: string | null + isModified: boolean + } +> + +export interface DiffResult { + stats: DiffStats + files: DiffFile[] +} diff --git a/packages/git/src/types.ts b/packages/git/src/types.ts index f5d44deb..096b99b0 100644 --- a/packages/git/src/types.ts +++ b/packages/git/src/types.ts @@ -1,8 +1,7 @@ -import * as Diff2Html from 'diff2html/lib-esm/types' - export * from './GitRefs.types' export * from './GitCommits.types' export * from './Watcher.types' +export * from './GitDiff.types' export interface StatusFile { path: string @@ -15,25 +14,6 @@ export interface StatusFile { isRenamed: boolean } -type Modify = Omit & R -export interface DiffStats { - insertions: number - deletions: number - filesChanged: number -} -export type DiffFile = Modify< - Diff2Html.DiffFile, - { - newName: string | null - oldName: string | null - isModified: boolean - } -> -export interface DiffResult { - stats: DiffStats - files: DiffFile[] -} - export type GitFileOp = | 'A' // Added | 'C' // Copied From a4a9d8d47e318a4c6f5a785500c3f59ea93be7b8 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 17 Oct 2021 21:06:55 +0100 Subject: [PATCH 3/9] Bring voer getFileText and rename methods --- packages/git/src/Git.ts | 41 +------------- packages/git/src/GitDiff.test.ts | 22 ++++---- packages/git/src/GitDiff.ts | 54 ++++++++++++++++--- packages/git/src/GitDiff.types.ts | 6 +++ packages/git/src/types.ts | 6 --- .../renderer/components/diff/useDiffData.ts | 12 ++--- 6 files changed, 71 insertions(+), 70 deletions(-) diff --git a/packages/git/src/Git.ts b/packages/git/src/Git.ts index 5cacd595..3e0458ba 100644 --- a/packages/git/src/Git.ts +++ b/packages/git/src/Git.ts @@ -8,7 +8,7 @@ import { spawn } from 'child_process' import { resolveRepo } from './resolve-repo' import { STATE, STATE_FILES } from './constants' -import type { GitFileOp, GetSpawn, StatusFile, FileText, Remote } from './types' +import type { GitFileOp, GetSpawn, StatusFile, Remote } from './types' import { GitRefs } from './GitRefs' import { GitCommits } from './GitCommits' @@ -268,43 +268,4 @@ export class Git { .sortBy((file: StatusFile) => file.path) .value() } - - getFilePlainText = async ( - filePath: string | null, - sha: string | null = null, - ): Promise => { - const spawn = await this._getSpawn() - if (!spawn) { - return null - } - - if (!filePath) { - return { - path: '', - type: '', - text: '', - } - } - - const fileType = path.extname(filePath) - - let plainText = null - if (sha) { - const cmd = ['show', `${sha}:${filePath}`] - plainText = await spawn(cmd) - } else { - const absoluteFilePath = path.join(this.cwd, filePath) - plainText = await new Promise((resolve, reject) => { - fs.readFile(absoluteFilePath, (err, data) => { - err ? reject(err) : resolve(data.toString()) - }) - }) - } - - return { - path: filePath, - text: plainText, - type: fileType, - } - } } diff --git a/packages/git/src/GitDiff.test.ts b/packages/git/src/GitDiff.test.ts index 5c5d3b78..71320b5d 100644 --- a/packages/git/src/GitDiff.test.ts +++ b/packages/git/src/GitDiff.test.ts @@ -32,7 +32,7 @@ describe('Git', () => { shim.writeFile('a.txt', 'line 1\nline 2') const sha = await shim.commit('Commit 1') - const diff = (await git.diff.getDiffFromShas( + const diff = (await git.diff.getByShas( sha, useOldSha ? baseSha : null, ))! @@ -67,7 +67,7 @@ describe('Git', () => { shim.renameFile('a.txt', 'b.txt') const sha = await shim.commit('Commit 2') - const diff = (await git.diff.getDiffFromShas( + const diff = (await git.diff.getByShas( sha, useOldSha ? baseSha : null, ))! @@ -101,7 +101,7 @@ describe('Git', () => { shim.writeFile('a.txt', 'line 1\nline 2') const sha = await shim.commit('Commit 2') - const diff = (await git.diff.getDiffFromShas( + const diff = (await git.diff.getByShas( sha, useOldSha ? baseSha : null, ))! @@ -136,7 +136,7 @@ describe('Git', () => { shim.rmFile('a.txt') const sha = await shim.commit('Commit 2') - const diff = (await git.diff.getDiffFromShas( + const diff = (await git.diff.getByShas( sha, useOldSha ? baseSha : null, ))! @@ -183,7 +183,7 @@ describe('Git', () => { await spawn(['add', '--all']) } - const diff = (await git.diff.getDiffFromIndex())! + const diff = (await git.diff.getIndex())! expect(diff).toEqual({ stats: { @@ -220,7 +220,7 @@ describe('Git', () => { await spawn(['add', '--all']) } - const diff = (await git.diff.getDiffFromIndex())! + const diff = (await git.diff.getIndex())! expect(diff).toEqual({ stats: { @@ -255,7 +255,7 @@ describe('Git', () => { await spawn(['add', '--all']) } - const diff = (await git.diff.getDiffFromIndex())! + const diff = (await git.diff.getIndex())! expect(diff).toEqual({ stats: { @@ -285,7 +285,7 @@ describe('Git', () => { shim.renameFile('a.txt', 'b.txt') - const diff = (await git.diff.getDiffFromIndex())! + const diff = (await git.diff.getIndex())! expect(diff).toEqual({ stats: { @@ -325,7 +325,7 @@ describe('Git', () => { shim.renameFile('a.txt', 'b.txt') shim.writeFile('b.txt', 'line 1\nline 3') - const diff = (await git.diff.getDiffFromIndex())! + const diff = (await git.diff.getIndex())! expect(diff).toEqual({ stats: { @@ -365,7 +365,7 @@ describe('Git', () => { shim.renameFile('a.txt', 'b.txt') await spawn(['add', '--all']) - const diff = (await git.diff.getDiffFromIndex())! + const diff = (await git.diff.getIndex())! expect(diff).toEqual({ stats: { @@ -396,7 +396,7 @@ describe('Git', () => { shim.writeFile('b.txt', 'line 1\nline 3') await spawn(['add', '--all']) - const diff = (await git.diff.getDiffFromIndex())! + const diff = (await git.diff.getIndex())! expect(diff).toEqual({ stats: { diff --git a/packages/git/src/GitDiff.ts b/packages/git/src/GitDiff.ts index 1263ca77..5b0e858b 100644 --- a/packages/git/src/GitDiff.ts +++ b/packages/git/src/GitDiff.ts @@ -1,13 +1,16 @@ +import fs from 'fs' +import path from 'path' import _ from 'lodash' import * as Diff2Html from 'diff2html' import { Git } from './Git' -import type { DiffFile, DiffResult, GetSpawn } from './types' +import type { DiffFile, DiffResult, FileText } from './GitDiff.types' +import type { GetSpawn } from './types' export class GitDiff { - _cwd: string - _getSpawn: GetSpawn + private _cwd: string + private _getSpawn: GetSpawn private git: Git constructor(cwd: string, getSpawn: GetSpawn, git: Git) { @@ -16,7 +19,46 @@ export class GitDiff { this.git = git } - getDiffFromShas = async ( + loadFileText = async ( + filePath: string | null, + sha: string | null = null, + ): Promise => { + const spawn = await this._getSpawn() + if (!spawn) { + return null + } + + if (!filePath) { + return { + path: '', + type: '', + text: '', + } + } + + const fileType = path.extname(filePath) + + let plainText = null + if (sha) { + const cmd = ['show', `${sha}:${filePath}`] + plainText = await spawn(cmd) + } else { + const absoluteFilePath = path.join(this._cwd, filePath) + plainText = await new Promise((resolve, reject) => { + fs.readFile(absoluteFilePath, (err, data) => { + err ? reject(err) : resolve(data.toString()) + }) + }) + } + + return { + path: filePath, + text: plainText, + type: fileType, + } + } + + getByShas = async ( shaNew: string, shaOld: string | null = null, { contextLines = 10 } = {}, @@ -54,9 +96,7 @@ export class GitDiff { return diff } - getDiffFromIndex = async ({ - contextLines = 5, - } = {}): Promise => { + getIndex = async ({ contextLines = 5 } = {}): Promise => { const spawn = await this._getSpawn() if (!spawn) { return null diff --git a/packages/git/src/GitDiff.types.ts b/packages/git/src/GitDiff.types.ts index 744dfb23..c835dd8e 100644 --- a/packages/git/src/GitDiff.types.ts +++ b/packages/git/src/GitDiff.types.ts @@ -21,3 +21,9 @@ export interface DiffResult { stats: DiffStats files: DiffFile[] } + +export interface FileText { + path: string + text: string + type: string +} diff --git a/packages/git/src/types.ts b/packages/git/src/types.ts index 096b99b0..d4126c69 100644 --- a/packages/git/src/types.ts +++ b/packages/git/src/types.ts @@ -26,12 +26,6 @@ export type GitFileOp = | 'B' // Broken | undefined -export interface FileText { - path: string - text: string - type: string -} - export interface SpawnOpts { okCodes?: number[] } diff --git a/packages/giterm/app/renderer/components/diff/useDiffData.ts b/packages/giterm/app/renderer/components/diff/useDiffData.ts index b2d158d1..28f17168 100644 --- a/packages/giterm/app/renderer/components/diff/useDiffData.ts +++ b/packages/giterm/app/renderer/components/diff/useDiffData.ts @@ -44,10 +44,10 @@ export function useDiffData({ contextLines = 5 } = {}): DiffData { const diff = mode === 'shas' - ? await git.diff.getDiffFromShas(shaNew, shaOld, { + ? await git.diff.getByShas(shaNew, shaOld, { contextLines, }) - : await git.diff.getDiffFromIndex({ contextLines }) + : await git.diff.getIndex({ contextLines }) if (!cancelled) { if (diff) { @@ -95,11 +95,11 @@ export function useDiffData({ contextLines = 5 } = {}): DiffData { let right: FileText | null if (mode === 'shas') { const shaOldRelative = shaOld ?? `${shaNew}~1` - left = await git.getFilePlainText(leftName, shaOldRelative) - right = await git.getFilePlainText(rightName, shaNew) + left = await git.diff.loadFileText(leftName, shaOldRelative) + right = await git.diff.loadFileText(rightName, shaNew) } else { - left = await git.getFilePlainText(leftName, 'HEAD') - right = await git.getFilePlainText(rightName) + left = await git.diff.loadFileText(leftName, 'HEAD') + right = await git.diff.loadFileText(rightName) } if (!cancelled) { From c0072c8db9359f264bb9038a02645923832a5d40 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 17 Oct 2021 21:24:20 +0100 Subject: [PATCH 4/9] Strip down DiffFile and DiffResult to exclude the meat of Diff2HTML. Index diff returned only via getStatus now. --- packages/git/src/GitDiff.ts | 73 +++++++++++-------- packages/git/src/GitDiff.types.ts | 22 +++--- .../app/renderer/components/diff/Files.tsx | 2 +- .../app/renderer/components/diff/types.ts | 21 ------ 4 files changed, 54 insertions(+), 64 deletions(-) delete mode 100644 packages/giterm/app/renderer/components/diff/types.ts diff --git a/packages/git/src/GitDiff.ts b/packages/git/src/GitDiff.ts index 5b0e858b..9ad11936 100644 --- a/packages/git/src/GitDiff.ts +++ b/packages/git/src/GitDiff.ts @@ -102,40 +102,52 @@ export class GitDiff { return null } - // TODO: can we demand this as an input instead to save on 200ms? const statusFiles = await this.git.getStatus() - const diffTexts = await Promise.all( - statusFiles.map(async (statusFile) => { - const cmd = ['diff', '--unified=' + contextLines] - - if (statusFile.isNew) { - // Has to be compared to an empty file - cmd.push('/dev/null', statusFile.path) - } else if (statusFile.isDeleted) { - // Has to be compared to current HEAD tree - cmd.push('HEAD', '--', statusFile.path) - } else if (statusFile.isModified) { - // Compare back to head - cmd.push('HEAD', statusFile.path) - } else if (statusFile.isRenamed) { - // Have to tell git diff about the rename - cmd.push('HEAD', '--', statusFile.oldPath!, statusFile.path) + return { + files: statusFiles.map((sf) => { + return { + newName: sf.path, + oldName: sf.oldPath, + isNew: sf.isNew, + isDeleted: sf.isDeleted, + isRenamed: sf.isRenamed, + isModified: sf.isModified, } - - return await spawn(cmd, { okCodes: [0, 1] }) }), - ) - - const diffText = diffTexts.join('\n') - - const diff = await this.processDiff(diffText) + } - return diff + // const diffTexts = await Promise.all( + // statusFiles.map(async (statusFile) => { + // const cmd = ['diff', '--unified=' + contextLines] + + // if (statusFile.isNew) { + // // Has to be compared to an empty file + // cmd.push('/dev/null', statusFile.path) + // } else if (statusFile.isDeleted) { + // // Has to be compared to current HEAD tree + // cmd.push('HEAD', '--', statusFile.path) + // } else if (statusFile.isModified) { + // // Compare back to head + // cmd.push('HEAD', statusFile.path) + // } else if (statusFile.isRenamed) { + // // Have to tell git diff about the rename + // cmd.push('HEAD', '--', statusFile.oldPath!, statusFile.path) + // } + + // return await spawn(cmd, { okCodes: [0, 1] }) + // }), + // ) + + // const diffText = diffTexts.join('\n') + + // const diff = await this.processDiff(diffText) + + // return diff } - private processDiff = async (diffText: string): Promise => { - const files = Diff2Html.parse(diffText) as DiffFile[] + private processDiff = async (diffText: string): Promise => { + const files = Diff2Html.parse(diffText) as any for (const file of files) { if (file.oldName === '/dev/null') { @@ -154,10 +166,13 @@ export class GitDiff { return { stats: { - insertions: files.reduce((result, file) => file.addedLines + result, 0), + insertions: files.reduce( + (result: any, file: any) => file.addedLines + result, + 0, + ), filesChanged: files.length, deletions: files.reduce( - (result, file) => file.deletedLines + result, + (result: any, file: any) => file.deletedLines + result, 0, ), }, diff --git a/packages/git/src/GitDiff.types.ts b/packages/git/src/GitDiff.types.ts index c835dd8e..820ecdab 100644 --- a/packages/git/src/GitDiff.types.ts +++ b/packages/git/src/GitDiff.types.ts @@ -1,24 +1,20 @@ -import * as Diff2Html from 'diff2html/lib-esm/types' - -type Modify = Omit & R - export interface DiffStats { insertions: number deletions: number filesChanged: number } -export type DiffFile = Modify< - Diff2Html.DiffFile, - { - newName: string | null - oldName: string | null - isModified: boolean - } -> +export type DiffFile = { + newName: string | null + oldName: string | null + isNew: boolean + isDeleted: boolean + isModified: boolean + isRenamed: boolean +} export interface DiffResult { - stats: DiffStats + // stats: DiffStats files: DiffFile[] } diff --git a/packages/giterm/app/renderer/components/diff/Files.tsx b/packages/giterm/app/renderer/components/diff/Files.tsx index 7fac1c15..6c1983dc 100644 --- a/packages/giterm/app/renderer/components/diff/Files.tsx +++ b/packages/giterm/app/renderer/components/diff/Files.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components' import { List, RightClickArea } from 'app/lib/primitives' import { colours } from 'app/lib/theme' -import { DiffResult } from './types' +import { DiffResult } from '@giterm/git' import { clipboard } from 'electron' interface Props { diff --git a/packages/giterm/app/renderer/components/diff/types.ts b/packages/giterm/app/renderer/components/diff/types.ts deleted file mode 100644 index d64523bd..00000000 --- a/packages/giterm/app/renderer/components/diff/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DiffResult, DiffFile } from '@giterm/git' - -export type { DiffResult } - -export type DiffLine = DiffFile['blocks'][0]['lines'][0] - -export type FilePatchBlock = DiffFile['blocks'][0] & { - linesLeft: DiffLine[] - linesRight: DiffLine[] -} - -type Modify = Omit & R -export type FilePatch = Modify< - DiffFile, - { - selectedFileName: string - blocks: FilePatchBlock[] - originalText: string - modifiedText: string - } -> From 6935477dff00e8eb19f4dd203a4398cd4db64ac5 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 17 Oct 2021 22:18:14 +0100 Subject: [PATCH 5/9] Break out diff parsing from getStatus and parallelise file fetching --- packages/git/src/Git.ts | 47 ++---- packages/git/src/GitDiff.test.ts | 106 ++---------- packages/git/src/GitDiff.ts | 154 ++++++++---------- packages/git/src/git-diff-parsing.ts | 27 +++ packages/git/src/git-diff-parsing.types.ts | 17 ++ packages/git/src/types.ts | 13 +- .../renderer/components/diff/useDiffData.ts | 20 +-- 7 files changed, 144 insertions(+), 240 deletions(-) create mode 100644 packages/git/src/git-diff-parsing.ts create mode 100644 packages/git/src/git-diff-parsing.types.ts diff --git a/packages/git/src/Git.ts b/packages/git/src/Git.ts index 3e0458ba..7b8ec49a 100644 --- a/packages/git/src/Git.ts +++ b/packages/git/src/Git.ts @@ -8,13 +8,14 @@ import { spawn } from 'child_process' import { resolveRepo } from './resolve-repo' import { STATE, STATE_FILES } from './constants' -import type { GitFileOp, GetSpawn, StatusFile, Remote } from './types' +import type { GetSpawn, StatusFile, Remote, FileInfo } from './types' import { GitRefs } from './GitRefs' import { GitCommits } from './GitCommits' import { GitDiff } from './GitDiff' import { Watcher } from './Watcher' import { perfStart, instrumentClass } from './performance' +import { parseDiffNameStatusViewWithNulColumns } from './git-diff-parsing' export class Git { rawCwd: string @@ -172,39 +173,15 @@ export class Git { return [] } - interface FileInfo { - staged: boolean - operation: GitFileOp - path1: string - path2?: string - } - - const parseNamesWithStaged = - (staged: boolean) => - (output: string): FileInfo[] => { - const segments = output.split('\0').filter(Boolean) - - const lines: [GitFileOp, string, string?][] = [] - while (segments.length > 0) { - const operation = segments.shift() as string - const operationKey = operation?.slice(0, 1) as GitFileOp - if (operationKey === 'R' || operationKey === 'C') { - const path1 = segments.shift()! - const path2 = segments.shift()! - lines.push([operationKey, path1, path2]) - } else if (operationKey) { - const path1 = segments.shift()! - lines.push([operationKey, path1]) - } - } + type FileInfoWithStaged = FileInfo & { staged: boolean } - return lines.map((line) => ({ - staged, - operation: line[0], - path1: line[1], - path2: line[2], - })) - } + const parseNamesWithStaged = (staged: boolean) => (output: string) => { + return parseDiffNameStatusViewWithNulColumns(output).map((fileInfo) => { + const extendedFileInfo = fileInfo as FileInfoWithStaged + extendedFileInfo.staged = staged + return extendedFileInfo + }) + } const stagedPromise = spawn([ 'diff', @@ -229,7 +206,7 @@ export class Git { return output .split('\0') .filter(Boolean) - .map((filepath) => ({ + .map((filepath) => ({ staged: false, operation: 'A', path1: filepath, @@ -244,7 +221,7 @@ export class Git { return _(results) .flatMap() - .map((file: FileInfo) => { + .map((file: FileInfoWithStaged) => { let filePath = null let oldFilePath = null if (file.operation === 'R') { diff --git a/packages/git/src/GitDiff.test.ts b/packages/git/src/GitDiff.test.ts index 71320b5d..5bc42a41 100644 --- a/packages/git/src/GitDiff.test.ts +++ b/packages/git/src/GitDiff.test.ts @@ -38,19 +38,12 @@ describe('Git', () => { ))! expect(diff).toEqual({ - stats: { - deletions: 0, - filesChanged: 1, - insertions: 2, - }, files: [ expect.objectContaining>({ oldName: null, newName: 'a.txt', - addedLines: 2, - deletedLines: 0, isNew: true, - isRename: false, + isRenamed: false, isDeleted: false, isModified: false, }), @@ -73,18 +66,12 @@ describe('Git', () => { ))! expect(diff).toEqual({ - stats: { - deletions: 0, - filesChanged: 1, - insertions: 0, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: 'b.txt', - addedLines: 0, isNew: false, - isRename: true, + isRenamed: true, isModified: true, isDeleted: false, }), @@ -107,19 +94,12 @@ describe('Git', () => { ))! expect(diff).toEqual({ - stats: { - deletions: 1, - filesChanged: 1, - insertions: 1, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: 'a.txt', - addedLines: 1, - deletedLines: 1, isNew: false, - isRename: false, + isRenamed: false, isModified: true, isDeleted: false, }), @@ -142,19 +122,12 @@ describe('Git', () => { ))! expect(diff).toEqual({ - stats: { - deletions: 2, - filesChanged: 1, - insertions: 0, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: null, - addedLines: 0, - deletedLines: 2, isNew: false, - isRename: false, + isRenamed: false, isModified: false, isDeleted: true, }), @@ -186,19 +159,12 @@ describe('Git', () => { const diff = (await git.diff.getIndex())! expect(diff).toEqual({ - stats: { - deletions: 0, - filesChanged: 1, - insertions: 2, - }, files: [ expect.objectContaining>({ oldName: null, newName: 'b.txt', - addedLines: 2, - deletedLines: 0, isNew: true, - isRename: false, + isRenamed: false, isDeleted: false, isModified: false, }), @@ -223,19 +189,12 @@ describe('Git', () => { const diff = (await git.diff.getIndex())! expect(diff).toEqual({ - stats: { - deletions: 1, - filesChanged: 1, - insertions: 1, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: 'a.txt', - addedLines: 1, - deletedLines: 1, isNew: false, - isRename: false, + isRenamed: false, isModified: true, isDeleted: false, }), @@ -258,19 +217,12 @@ describe('Git', () => { const diff = (await git.diff.getIndex())! expect(diff).toEqual({ - stats: { - deletions: 2, - filesChanged: 1, - insertions: 0, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: null, - addedLines: 0, - deletedLines: 2, isNew: false, - isRename: false, + isRenamed: false, isModified: false, isDeleted: true, }), @@ -288,29 +240,20 @@ describe('Git', () => { const diff = (await git.diff.getIndex())! expect(diff).toEqual({ - stats: { - deletions: 2, - filesChanged: 2, - insertions: 2, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: null, - addedLines: 0, - deletedLines: 2, isNew: false, - isRename: false, + isRenamed: false, isModified: false, isDeleted: true, }), expect.objectContaining>({ oldName: null, newName: 'b.txt', - addedLines: 2, - deletedLines: 0, isNew: true, - isRename: false, + isRenamed: false, isModified: false, isDeleted: false, }), @@ -328,29 +271,20 @@ describe('Git', () => { const diff = (await git.diff.getIndex())! expect(diff).toEqual({ - stats: { - deletions: 2, - filesChanged: 2, - insertions: 2, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: null, - addedLines: 0, - deletedLines: 2, isNew: false, - isRename: false, + isRenamed: false, isModified: false, isDeleted: true, }), expect.objectContaining>({ oldName: null, newName: 'b.txt', - addedLines: 2, - deletedLines: 0, isNew: true, - isRename: false, + isRenamed: false, isModified: false, isDeleted: false, }), @@ -368,19 +302,12 @@ describe('Git', () => { const diff = (await git.diff.getIndex())! expect(diff).toEqual({ - stats: { - deletions: 0, - filesChanged: 1, - insertions: 0, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: 'b.txt', - addedLines: 0, - deletedLines: 0, isNew: false, - isRename: true, + isRenamed: true, isModified: true, isDeleted: false, }), @@ -399,19 +326,12 @@ describe('Git', () => { const diff = (await git.diff.getIndex())! expect(diff).toEqual({ - stats: { - deletions: 1, - filesChanged: 1, - insertions: 1, - }, files: [ expect.objectContaining>({ oldName: 'a.txt', newName: 'b.txt', - addedLines: 1, - deletedLines: 1, isNew: false, - isRename: true, + isRenamed: true, isModified: true, isDeleted: false, }), diff --git a/packages/git/src/GitDiff.ts b/packages/git/src/GitDiff.ts index 9ad11936..0c9b1f67 100644 --- a/packages/git/src/GitDiff.ts +++ b/packages/git/src/GitDiff.ts @@ -1,12 +1,12 @@ import fs from 'fs' import path from 'path' import _ from 'lodash' -import * as Diff2Html from 'diff2html' import { Git } from './Git' import type { DiffFile, DiffResult, FileText } from './GitDiff.types' import type { GetSpawn } from './types' +import { parseDiffNameStatusViewWithNulColumns } from './git-diff-parsing' export class GitDiff { private _cwd: string @@ -61,42 +61,62 @@ export class GitDiff { getByShas = async ( shaNew: string, shaOld: string | null = null, - { contextLines = 10 } = {}, ): Promise => { const spawn = await this._getSpawn() if (!spawn) { return null } - // From Git: - // git diff SHAOLD SHANEW --unified=10 - // git show SHA --patch -m - if (!shaNew) { console.error('shaNew was not provided') return null } - let cmd = [] - if (shaOld) { - cmd = ['diff', shaOld, shaNew, '--unified=' + contextLines] - } else { - cmd = [ - 'show', - shaNew, - '--patch', // Always show patch - '-m', // Show patch even on merge commits - '--unified=' + contextLines, - ] + if (!shaOld) { + shaOld = `${shaNew}~1` } + const cmd = [ + 'diff', + shaOld, + shaNew, + '--name-status', + '-z', // Terminate columns with NUL + ] + + const output = await spawn(cmd) + const files = parseDiffNameStatusViewWithNulColumns(output) - const patchText = await spawn(cmd) - const diff = await this.processDiff(patchText) + return { + files: files.map((file) => { + let newName: string | null = null + let oldName: string | null = null + if (file.operation === 'R') { + oldName = file.path1.trim() + newName = file.path2!.trim() + } else if (file.operation === 'A') { + newName = file.path1.trim() + oldName = null + } else if (file.operation === 'D') { + newName = null + oldName = file.path1.trim() + } else { + oldName = file.path1.trim() + newName = file.path1.trim() + } - return diff + return { + newName: newName, + oldName: oldName, + isDeleted: file.operation === 'D', + isModified: file.operation === 'M' || file.operation === 'R', + isNew: file.operation === 'A', + isRenamed: file.operation === 'R', + } + }), + } } - getIndex = async ({ contextLines = 5 } = {}): Promise => { + getIndex = async (): Promise => { const spawn = await this._getSpawn() if (!spawn) { return null @@ -105,78 +125,32 @@ export class GitDiff { const statusFiles = await this.git.getStatus() return { - files: statusFiles.map((sf) => { + files: statusFiles.map((file) => { + let newName: string | null = null + let oldName: string | null = null + if (file.isRenamed) { + oldName = file.oldPath + newName = file.path + } else if (file.isNew) { + newName = file.path + oldName = null + } else if (file.isDeleted) { + newName = null + oldName = file.path + } else { + oldName = file.path + newName = file.path + } + return { - newName: sf.path, - oldName: sf.oldPath, - isNew: sf.isNew, - isDeleted: sf.isDeleted, - isRenamed: sf.isRenamed, - isModified: sf.isModified, + newName: newName, + oldName: oldName, + isNew: file.isNew, + isDeleted: file.isDeleted, + isRenamed: file.isRenamed, + isModified: file.isModified || file.isRenamed, } }), } - - // const diffTexts = await Promise.all( - // statusFiles.map(async (statusFile) => { - // const cmd = ['diff', '--unified=' + contextLines] - - // if (statusFile.isNew) { - // // Has to be compared to an empty file - // cmd.push('/dev/null', statusFile.path) - // } else if (statusFile.isDeleted) { - // // Has to be compared to current HEAD tree - // cmd.push('HEAD', '--', statusFile.path) - // } else if (statusFile.isModified) { - // // Compare back to head - // cmd.push('HEAD', statusFile.path) - // } else if (statusFile.isRenamed) { - // // Have to tell git diff about the rename - // cmd.push('HEAD', '--', statusFile.oldPath!, statusFile.path) - // } - - // return await spawn(cmd, { okCodes: [0, 1] }) - // }), - // ) - - // const diffText = diffTexts.join('\n') - - // const diff = await this.processDiff(diffText) - - // return diff - } - - private processDiff = async (diffText: string): Promise => { - const files = Diff2Html.parse(diffText) as any - - for (const file of files) { - if (file.oldName === '/dev/null') { - file.oldName = null - } - if (file.newName === '/dev/null') { - file.newName = null - } - - // Diff2Html doesn't attach false values, so patch these on - file.isNew = !!file.isNew - file.isDeleted = !!file.isDeleted - file.isRename = !!file.isRename - file.isModified = !file.isNew && !file.isDeleted - } - - return { - stats: { - insertions: files.reduce( - (result: any, file: any) => file.addedLines + result, - 0, - ), - filesChanged: files.length, - deletions: files.reduce( - (result: any, file: any) => file.deletedLines + result, - 0, - ), - }, - files, - } } } diff --git a/packages/git/src/git-diff-parsing.ts b/packages/git/src/git-diff-parsing.ts new file mode 100644 index 00000000..0d4d3863 --- /dev/null +++ b/packages/git/src/git-diff-parsing.ts @@ -0,0 +1,27 @@ +import { GitFileOp, FileInfo } from './git-diff-parsing.types' + +export function parseDiffNameStatusViewWithNulColumns( + output: string, +): FileInfo[] { + const segments = output.split('\0').filter(Boolean) + + const lines: [GitFileOp, string, string?][] = [] + while (segments.length > 0) { + const operation = segments.shift() as string + const operationKey = operation?.slice(0, 1) as GitFileOp + if (operationKey === 'R' || operationKey === 'C') { + const path1 = segments.shift()! + const path2 = segments.shift()! + lines.push([operationKey, path1, path2]) + } else if (operationKey) { + const path1 = segments.shift()! + lines.push([operationKey, path1]) + } + } + + return lines.map((line) => ({ + operation: line[0], + path1: line[1], + path2: line[2], + })) +} diff --git a/packages/git/src/git-diff-parsing.types.ts b/packages/git/src/git-diff-parsing.types.ts new file mode 100644 index 00000000..6f1857b4 --- /dev/null +++ b/packages/git/src/git-diff-parsing.types.ts @@ -0,0 +1,17 @@ +export type GitFileOp = + | 'A' // Added + | 'C' // Copied + | 'D' // Deleted + | 'M' // Modified + | 'R' // Renamed + | 'T' // Type changed + | 'U' // Unmerged + | 'X' // Unknown + | 'B' // Broken + | undefined + +export interface FileInfo { + operation: GitFileOp + path1: string + path2?: string +} diff --git a/packages/git/src/types.ts b/packages/git/src/types.ts index d4126c69..5be077e6 100644 --- a/packages/git/src/types.ts +++ b/packages/git/src/types.ts @@ -2,6 +2,7 @@ export * from './GitRefs.types' export * from './GitCommits.types' export * from './Watcher.types' export * from './GitDiff.types' +export * from './git-diff-parsing.types' export interface StatusFile { path: string @@ -14,18 +15,6 @@ export interface StatusFile { isRenamed: boolean } -export type GitFileOp = - | 'A' // Added - | 'C' // Copied - | 'D' // Deleted - | 'M' // Modified - | 'R' // Renamed - | 'T' // Type changed - | 'U' // Unmerged - | 'X' // Unknown - | 'B' // Broken - | undefined - export interface SpawnOpts { okCodes?: number[] } diff --git a/packages/giterm/app/renderer/components/diff/useDiffData.ts b/packages/giterm/app/renderer/components/diff/useDiffData.ts index 28f17168..9901eae6 100644 --- a/packages/giterm/app/renderer/components/diff/useDiffData.ts +++ b/packages/giterm/app/renderer/components/diff/useDiffData.ts @@ -44,10 +44,8 @@ export function useDiffData({ contextLines = 5 } = {}): DiffData { const diff = mode === 'shas' - ? await git.diff.getByShas(shaNew, shaOld, { - contextLines, - }) - : await git.diff.getIndex({ contextLines }) + ? await git.diff.getByShas(shaNew, shaOld) + : await git.diff.getIndex() if (!cancelled) { if (diff) { @@ -91,17 +89,19 @@ export function useDiffData({ contextLines = 5 } = {}): DiffData { async function fetch() { const git = new Git(cwd) - let left: FileText | null - let right: FileText | null + let leftPromise: Promise + let rightPromise: Promise if (mode === 'shas') { const shaOldRelative = shaOld ?? `${shaNew}~1` - left = await git.diff.loadFileText(leftName, shaOldRelative) - right = await git.diff.loadFileText(rightName, shaNew) + leftPromise = git.diff.loadFileText(leftName, shaOldRelative) + rightPromise = git.diff.loadFileText(rightName, shaNew) } else { - left = await git.diff.loadFileText(leftName, 'HEAD') - right = await git.diff.loadFileText(rightName) + leftPromise = git.diff.loadFileText(leftName, 'HEAD') + rightPromise = git.diff.loadFileText(rightName) } + const [left, right] = await Promise.all([leftPromise, rightPromise]) + if (!cancelled) { if (left && right) { setLeft(left) From 1b13eda6deb56dbdb760607e9d561e08c031d530 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 17 Oct 2021 22:27:20 +0100 Subject: [PATCH 6/9] Decouple line feeds in terminal from CWD refresh --- .../giterm/app/renderer/components/terminal/Terminal.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/giterm/app/renderer/components/terminal/Terminal.tsx b/packages/giterm/app/renderer/components/terminal/Terminal.tsx index 12ceec9c..d58c5f68 100644 --- a/packages/giterm/app/renderer/components/terminal/Terminal.tsx +++ b/packages/giterm/app/renderer/components/terminal/Terminal.tsx @@ -190,13 +190,15 @@ export function Terminal({ isShown = true, onAlternateBufferChange }: Props) { // Refresh immediately after input const onNewLineDisposable = terminal.onKey(async (e) => { if (e.domEvent.code === 'Enter') { - await handleTerminalUpdates() + // Yes this is an async function. No we don't want to wait for it to complete as that blocks the terminal! + handleTerminalUpdates() } }) // Refresh after new lines received - const onLineFeedDisposable = terminal.onLineFeed(async () => { - await handleTerminalUpdates() + const onLineFeedDisposable = terminal.onLineFeed(() => { + // Yes this is an async function. No we don't want to wait for it to complete as that blocks the terminal! + handleTerminalUpdates() }) // Refresh after long running processes finish From f78c503723933c2903941dac53424621d7380d1e Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 17 Oct 2021 22:33:41 +0100 Subject: [PATCH 7/9] =?UTF-8?q?Remove=20diff2html.=20You=20were=20awesome?= =?UTF-8?q?=20but=20Monaco=20is=20better=20for=20now=20=E2=9D=A4=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/giterm/package.json | 1 - yarn.lock | 45 ------------------------------------ 2 files changed, 46 deletions(-) diff --git a/packages/giterm/package.json b/packages/giterm/package.json index 8fb05ecb..69947b76 100644 --- a/packages/giterm/package.json +++ b/packages/giterm/package.json @@ -40,7 +40,6 @@ "chokidar": "^3.3.1", "color": "^3.1.2", "debounce": "^1.2.0", - "diff2html": "^3.4.11", "electron-devtools-installer": "^3.2.0", "electron-log": "^4.0.6", "electron-updater": "^4.2.2", diff --git a/yarn.lock b/yarn.lock index fe1c0f9f..626f4e02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3891,11 +3891,6 @@ abab@^2.0.0, abab@^2.0.3, abab@^2.0.5: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - abort-controller@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -7441,21 +7436,6 @@ diff-sequences@^27.0.6: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== -diff2html@^3.4.11: - version "3.4.13" - resolved "https://registry.yarnpkg.com/diff2html/-/diff2html-3.4.13.tgz#a19fb9e47b05b7ed573590d57a45e7a132d3dd9c" - integrity sha512-IQb+P3aDVjjctcpRF089E9Uxjb6JInu/1SDklLaw2KapdwXKl3xd87mieweR2h6hNvdyAlylMHRrwK8M4oV1Sw== - dependencies: - diff "5.0.0" - hogan.js "3.0.2" - optionalDependencies: - highlight.js "11.2.0" - -diff@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -10796,11 +10776,6 @@ hicat@^0.8.0: highlight.js "^10.4.1" minimist "^1.2.5" -highlight.js@11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.2.0.tgz#a7e3b8c1fdc4f0538b93b2dc2ddd53a40c6ab0f0" - integrity sha512-JOySjtOEcyG8s4MLR2MNbLUyaXqUunmSnL2kdV/KuGJOmHZuAR5xC54Ko7goAXBWNhf09Vy3B+U7vR62UZ/0iw== - highlight.js@^10.4.1: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" @@ -10827,14 +10802,6 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hogan.js@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd" - integrity sha1-TNnhq9QpQUbnZ55B14mHMrAse/0= - dependencies: - mkdirp "0.3.0" - nopt "1.0.10" - hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -14503,11 +14470,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" - integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4= - mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" @@ -14828,13 +14790,6 @@ noms@0.0.0: inherits "^2.0.1" readable-stream "~1.0.31" -nopt@1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= - dependencies: - abbrev "1" - normalize-package-data@^2.3.2: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" From 56fe8ebeab9834e1fa7f5fdc94f41d30952a5784 Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sun, 17 Oct 2021 22:38:05 +0100 Subject: [PATCH 8/9] Fix GitRefs layout issue where emoji are used in commit messages --- .../giterm/app/renderer/components/commits/Commits.tsx | 2 +- packages/giterm/app/renderer/components/commits/Row.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/giterm/app/renderer/components/commits/Commits.tsx b/packages/giterm/app/renderer/components/commits/Commits.tsx index 7ecc3e42..88357dee 100644 --- a/packages/giterm/app/renderer/components/commits/Commits.tsx +++ b/packages/giterm/app/renderer/components/commits/Commits.tsx @@ -33,7 +33,7 @@ export function Commits() { key: 'graph', width: `${GraphIndent + GraphColumnWidth * graphCols}px`, }, - { name: 'SHA', key: 'sha7', width: '50px' }, + { name: 'SHA', key: 'sha7', width: '55px' }, { name: 'Message', key: 'message', width: '500px', showTags: true }, { name: 'Author', key: 'authorStr', width: '150px' }, { diff --git a/packages/giterm/app/renderer/components/commits/Row.tsx b/packages/giterm/app/renderer/components/commits/Row.tsx index f4cfeb82..bf739283 100644 --- a/packages/giterm/app/renderer/components/commits/Row.tsx +++ b/packages/giterm/app/renderer/components/commits/Row.tsx @@ -45,11 +45,13 @@ const RowWrapper = styled.div` ` interface RowColumnProps { + alignVertical: boolean width: string } const RowColumn = styled.div` display: flex; flex-direction: row; + align-items: ${({ alignVertical }) => (alignVertical ? 'center' : 'initial')}; margin-right: 10px; max-height: 100%; @@ -128,7 +130,10 @@ export const _Row = ({ return ( {columns.map((column) => ( - + {column.showTags && localBranchRefs.map((branch) => ( Date: Sun, 17 Oct 2021 22:59:31 +0100 Subject: [PATCH 9/9] Increment version --- packages/giterm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/giterm/package.json b/packages/giterm/package.json index 69947b76..1e5c9249 100644 --- a/packages/giterm/package.json +++ b/packages/giterm/package.json @@ -1,6 +1,6 @@ { "name": "giterm", - "version": "0.17.1", + "version": "0.18.0", "description": "A git gui for terminal lovers", "homepage": "https://github.com/Nick-Lucas/giterm", "main": "init.js",