Branch

operations.ts

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
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
b8a3bb Tucker McKnight
268685 Tucker McKnight
b8a3bb Tucker McKnight
fb82fe Tucker McKnight
c945a9 Tucker McKnight
b8a3bb Tucker McKnight
c945a9 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
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
7e40e5 Tucker McKnight
c945a9 Tucker McKnight
b8a3bb Tucker McKnight
fb82fe Tucker McKnight
b8a3bb Tucker McKnight
c945a9 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
7e40e5 Tucker McKnight
fb82fe Tucker McKnight
fb82fe Tucker McKnight
fb82fe Tucker McKnight
fb82fe Tucker McKnight
fb82fe Tucker McKnight
fb82fe Tucker McKnight
7e40e5 Tucker McKnight
7e40e5 Tucker McKnight
fb82fe Tucker McKnight
7e40e5 Tucker McKnight
7e40e5 Tucker McKnight
268685 Tucker McKnight
4c0630 tucker
4c0630 tucker
4c0630 tucker
4c0630 tucker
4c0630 tucker
4c0630 tucker
4c0630 tucker
4c0630 tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
7e40e5 tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
7e40e5 tucker
b8a3bb tucker
7e40e5 tucker
7e40e5 tucker
7e40e5 tucker
fb82fe tucker
fb82fe tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
fb82fe tucker
b8a3bb tucker
c945a9 tucker
c945a9 tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
b8a3bb tucker
import util from 'util'
import childProcess from 'child_process'
const exec = util.promisify(childProcess.exec)
import { getGitDiffsFromPatchText} from '../../helpers.ts'
import { type Repository } from '../../dataTypes.ts'

export const getFileList = async (branchName: string, repoLocation: 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 != ".")
  // TODO: this could be better. This is adding each sub-path of a file to a set, so that
  // we don't wind up with repeats, and then converting that back into an array. E.g. if
  // we have two files:
  // - posts/blog/one.md
  // - posts/blog/two.md
  // this will add the following to the set:
  // posts, posts/blog, posts/blog/one.md, posts, posts/blog, posts/blog/two.md
  // The repeats will be omitted because it's a Set, and the resulting array will
  // be [posts, posts/blog, posts/blog/one.md, posts/blog/two.md].
  // This is because it's convenient to have the directories show up as their own "file"
  // in the file list, even though git doesn't treat them that way.
  const fileSet: Set<string> = new Set()
  files.forEach((file) => {
    const fileParts = file.split("/")
    const allPathsInFile = fileParts.reduce((accumulator, currentValue, index) => {
      // Skip the first iteration, we have already added it as the initialValue
      if (index === 0) { return accumulator }

      accumulator.push(accumulator[accumulator.length - 1] + '/' + currentValue)
      return accumulator
    }, [fileParts[0]])

    allPathsInFile.forEach((path) => {
      fileSet.add(path)
    })
  })
  return Array.from(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 previousHash: null | string = null
  for (let i = 0; i < totalPatchesCount; i = i + 10) {
    const gitLogSubsetRes = await exec(`git -C ${repoLocation} log ${branchName} -p -n 10 --skip ${i}`)
    let gitLogSubset = gitLogSubsetRes.stdout.split("\n")
    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()

      // set the parent hash of the previous commit, unless we're on the first commit
      if (previousHash !== null) {
        commits.get(previousHash)['parent'] = hash
      }
      previousHash = hash

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

      let author: string, date: string
      [1, 2, 3].forEach((lineNumber) => {
        if (currentPatch[lineNumber].startsWith("Author")) {
          author = currentPatch[lineNumber].replace("Author: ", "").trim()
        }
        else if (currentPatch[lineNumber].startsWith("Date")) {
          date = currentPatch[lineNumber].replace("Date: ", "").trim()
        }
      })
      const diffStart = currentPatch.findIndex((line) => {
        return line.startsWith("diff ")
      })
      // Git log is indent four spaces by default -- remove those.
      const commitMessage = currentPatch.slice(4, 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()

      const diffs = getGitDiffsFromPatchText(currentPatch.slice(diffStart).join("\n"))
      commits.set(hash, {
        hash,
        message: commitMessage,
        author,
        date: new Date(date),
        diffs,
        parent: null,
      })
    } while (gitLogSubset.length > 1)
  }
}

export const getFileLastTouchInfo = async (branch: string, filename: string, repoLocation: string) => {
  const regex = RegExp(".* [0-9]+ [0-9]+")
  const command = `git -C ${repoLocation} blame --porcelain ${branch} ${filename}`
  const res = await exec(command)
  const output = res.stdout
  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 = []
  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
}