Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/tidy up diffing #138

Merged
merged 9 commits into from
Oct 17, 2021
408 changes: 1 addition & 407 deletions packages/git/src/Git.test.ts

Large diffs are not rendered by default.

207 changes: 16 additions & 191 deletions packages/git/src/Git.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import _ from 'lodash'
import * as Diff2Html from 'diff2html'
import chokidar from 'chokidar'
import path from 'path'

Expand All @@ -9,20 +8,14 @@ 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 { 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
Expand All @@ -31,6 +24,7 @@ export class Git {

readonly refs: GitRefs
readonly commits: GitCommits
readonly diff: GitDiff
readonly watcher: Watcher

constructor(cwd: string) {
Expand All @@ -39,11 +33,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 () => {
Expand Down Expand Up @@ -177,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',
Expand All @@ -234,7 +206,7 @@ export class Git {
return output
.split('\0')
.filter(Boolean)
.map<FileInfo>((filepath) => ({
.map<FileInfoWithStaged>((filepath) => ({
staged: false,
operation: 'A',
path1: filepath,
Expand All @@ -249,7 +221,7 @@ export class Git {

return _(results)
.flatMap()
.map<StatusFile>((file: FileInfo) => {
.map<StatusFile>((file: FileInfoWithStaged) => {
let filePath = null
let oldFilePath = null
if (file.operation === 'R') {
Expand All @@ -273,151 +245,4 @@ export class Git {
.sortBy((file: StatusFile) => file.path)
.value()
}

getFilePlainText = async (
filePath: string | null,
sha: string | null = null,
): Promise<FileText | null> => {
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<string>((resolve, reject) => {
fs.readFile(absoluteFilePath, (err, data) => {
err ? reject(err) : resolve(data.toString())
})
})
}

return {
path: filePath,
text: plainText,
type: fileType,
}
}

getDiffFromShas = async (
shaNew: string,
shaOld: string | null = null,
{ contextLines = 10 } = {},
): Promise<DiffResult | null> => {
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<DiffResult | null> => {
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<DiffResult> => {
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,
}
}
}
Loading