Skip to content

Commit

Permalink
Merge pull request #138 from Nick-Lucas/chore/tidy-up-diffing
Browse files Browse the repository at this point in the history
Chore/tidy up diffing
  • Loading branch information
Nick-Lucas authored Oct 17, 2021
2 parents 7629fc6 + a3fafb7 commit b57d523
Show file tree
Hide file tree
Showing 16 changed files with 610 additions and 721 deletions.
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

0 comments on commit b57d523

Please sign in to comment.