Files snapshot from main

./src/vcses/git/operations.ts

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
330625 Tucker McKnight
f7c4e5 Tucker McKnight
b8a3bb Tucker McKnight
cdb797 Tucker McKnight
cdb797 Tucker McKnight
f7c4e5 Tucker McKnight
c945a9 Tucker McKnight
b8a3bb Tucker McKnight
c945a9 Tucker McKnight
b8a3bb Tucker McKnight
f7c4e5 Tucker McKnight
f7c4e5 Tucker McKnight
f7c4e5 Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
7e40e5 Tucker McKnight
c945a9 Tucker McKnight
b8a3bb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
b8a3bb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
b8a3bb Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
7e40e5 Tucker McKnight
2a54fe Tucker McKnight
fb82fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
18d3c9 Tucker McKnight
2a54fe Tucker McKnight
7e40e5 Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
330625 Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
c759cb Tucker McKnight
2a54fe Tucker McKnight
2a54fe Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
cdb797 Tucker McKnight
cdb797 Tucker McKnight
cdb797 Tucker McKnight
cdb797 Tucker McKnight
cdb797 Tucker McKnight
cdb797 Tucker McKnight
0bcb12 Tucker McKnight
cdb797 Tucker McKnight
b8a3bb Tucker McKnight
330625 Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
cdb797 Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
import util from 'util'
import childProcess from 'child_process'
const exec = util.promisify(childProcess.exec)
import { getGitDiffsFromPatchText, readChunkedCommandOutput } from '../../helpers.ts'
import { type Repository } from '../../dataTypes.ts'

export const getFileList = async (
  branchName: string, repoLocation: string
) : Promise<Set<string>> => {
  const command = `git -C ${repoLocation} ls-tree -r --name-only ${branchName}`

  const result = await exec(command)
  let files = result.stdout.split("\n").filter(item => item.length > 0 && item != ".")
  const fileSet: Set<string> = new Set(files)
  
  return fileSet
}

export const addBranchToCommitsMap = async(branchName: string, repoLocation: string, commits: Repository['commits']): Promise<void> => {
  const totalPatchesCountRes = await exec(`git -C ${repoLocation} rev-list --count ${branchName}`)
  const totalPatchesCount = parseInt(totalPatchesCountRes.stdout)
  let gitLogSubsetCmd = null
  let gitLogSubset = []
  return addBranchToCommitsMapWithSkip(branchName, repoLocation, commits, totalPatchesCount)
}

export const addBranchToCommitsMapWithSkip = async(branchName: string, repoLocation: string, commits: Repository['commits'], totalPatchesCount: number, skip = 0): Promise<void> => {
  return new Promise((resolve) => {
    if (skip >= totalPatchesCount) {
      return resolve()
    }

    const gitLogSubsetCmd = childProcess.spawn('git', ['-C', repoLocation, 'log', branchName, '--format=raw', '-p', '-n', '10', '--skip', skip.toString()], {
      stdio: [0, "pipe", "inherit"],
    })

    let gitLogSubset = []
    gitLogSubsetCmd.stdout.on('data', (data) => {
      gitLogSubset.push(data)
    })

    gitLogSubsetCmd.on('close', () => {
      addGitLogSubsetToMap(gitLogSubset.join('').split('\n'), commits)
      return addBranchToCommitsMapWithSkip(branchName, repoLocation, commits, totalPatchesCount, skip + 10).then(() => {
        return resolve()
      })
    })
  })
}

const addGitLogSubsetToMap = (gitLogSubset: string[], commits: Repository['commits']) => {
  do {
    const nextPatchStart = gitLogSubset.findIndex((line, index) => {
      return (index > 0 && line.startsWith("commit ")) || index === gitLogSubset.length - 1
    })
    const isEndOfFile = nextPatchStart === gitLogSubset.length - 1
    const currentPatch = gitLogSubset.slice(0, isEndOfFile ? nextPatchStart : nextPatchStart - 1)
    gitLogSubset = gitLogSubset.slice(nextPatchStart)

    const hash = currentPatch[0].replace("commit ", "").trim()

    // exit early if the commits map already includes this hash
    if (commits.has(hash)) {
      continue
    }

    const nextBlankLine = currentPatch.findIndex(line => line === "") || currentPatch.length
    const commitInfoSection = currentPatch.slice(0, nextBlankLine)

    let author: string, date: string
    let isMerge = false
    let parent: string | null = null

    commitInfoSection.forEach((commitInfoLine) => {
      if (commitInfoLine.startsWith("author")) {
        const lineWords = commitInfoLine.replace("author ", "").trim().split(' ')
        author = lineWords.slice(0, lineWords.length - 2).join(' ')
        date = lineWords[lineWords.length - 2]
      }
      if (commitInfoLine.startsWith("parent")) {
        parent = commitInfoLine.replace("parent ", "").trim()
      }
    })

    // Mark as a merge if more than one line starts with "parent"
    if (commitInfoSection.filter(line => line.startsWith("parent")).length > 1) {
      isMerge = true
    }

    let diffStart = currentPatch.findIndex((line) => {
      return line.startsWith("diff ")
    })

    if (diffStart === -1) {
      diffStart = currentPatch.length
    }
    // Git log is indent four spaces by default -- remove those.
    const commitMessage = currentPatch.slice(nextBlankLine + 1, diffStart).map(str => str.replace("    ", "")).filter((line) => {
      // git log --porcelain output adds these "Ignore-this:" lines and I'm not sure what they are
      return !line.startsWith('Ignore-this: ')
    }).join("\n").trim()

    // TODO: this is very slow on commits that have a huge 20k line package-lock.json change
    const diffs = getGitDiffsFromPatchText(currentPatch.slice(diffStart).join("\n"))
    commits.set(hash, {
      hash,
      message: commitMessage,
      isMerge: isMerge,
      author,
      date: new Date(parseInt(date) * 1000), // git timestamp is number of seconds,
                                             // js Date() exects milliseconds
      diffs,
      parent,
    })
  } while (gitLogSubset.length > 1)
}

export type BlameInfo = {
  sha: string,
  author: string,
}

export const getFileLastTouchInfo = async (
  filename: string, sha: string, repoLocation: string
) : Promise<Array<BlameInfo>> => {
  const regex = RegExp(".* [0-9]+ [0-9]+")
  const output = await readChunkedCommandOutput('git', ['-C', repoLocation, 'blame', '--porcelain', sha, filename])
  const outputLines = output.split("\n")
  const initialValue: Array<Array<string>> = [[outputLines[0]]]
  const chunked = outputLines.reduce((accumulator, currentLine, currentIndex) => {
    // skip the first iteration since we already gave it initialValue
    if (currentIndex == 0) { return accumulator }

    if (currentLine.match(regex)) {
      accumulator.push([currentLine])
      return accumulator
    }
    else {
      accumulator[accumulator.length - 1].push(currentLine)
      return accumulator
    }
  }, initialValue)

  let currentAuthor = ''
  let authorsAndShasByLine: Array<BlameInfo> = []
  chunked.forEach((chunk) => {
    let shaAndLineNumberParts = chunk[0].split(' ')
    let line = parseInt(shaAndLineNumberParts[2])
    let sha = shaAndLineNumberParts[0]
    if (chunk[1] && chunk[1].startsWith('author ')) {
      currentAuthor = chunk[1].replace('author ', '')
    }
    authorsAndShasByLine[line - 1] = {sha, author: currentAuthor}
  })
  return authorsAndShasByLine
}