Move patch diff method to a new file

Mon Mar 10 2025

tucker.mcknight@gmail.com

Also remove the filter for getting a patch's contents;
instead, get that right away when the initial parsing of
the log is happening.

Doing it in a separate step (meaning you need to make N cli
calls for N patches in the repo) takes forever on large
repos (like darcs.net). Adding the -v flag to `darcs log`
gives the diff output in that call, so we can get the
diffs and save them in the `repos` global data at
the same time.

Also, this change makes `darcs log` run with the --index
flag, which is used to check only a limited number of
results in the log output. Again, large repos (darcs.net)
were being a problem, exceeing the nodejs max string buffer
size when trying to read the entire `darcs log` output in
one go. So now it reads 100 at a time and loops through
until the log is empty.

bd9ace5a2fc0c0c685e6f37a3b7938c4af982f6f

darcs pull https://repos.tuckerm.us/repos/eleventy-darcs/branches/main -h bd9ace5a2fc0c0c685e6f37a3b7938c4af982f6f
_data/repos.js:6
Before
After
import { getDiffsFromPatchText } from '../helpers.js'

let repos = null
_data/repos.js:11
Before
  const repos =await Promise.all(Object.keys(darcsConfig.repos).map(async (repoName) => {
      const repoLocation = darcsConfig.repos[repoName].location
      const filesRes = await exec(`(cd ${repoLocation}; darcs show files)`)
      const files = filesRes.stdout.split("\n").filter(item => item.length > 0 && item != ".")

      const patchesRes = await exec(`(cd ${repoLocation}; darcs log --xml-output)`)
      const xmlString = patchesRes.stdout
    const jsonString = xmlJs.xml2json(xmlString)
    const logs = JSON.parse(jsonString)
        const patches = logs.elements[0].elements.map((element) => {
            const name = element.elements.find(elem = > elem.name == "name").elements.find(elem => elem.type = ="text").text
      return {
        details: element.attributes, name: name,
            
After
  if (repos === null) {
    repos = await Promise.all(Object.keys(darcsConfig.repos).map(async (repoName) => {
      const repoLocation = darcsConfig.repos[repoName].location
      const filesRes = await exec(`(cd ${repoLocation}; darcs show files)`)
      const files = filesRes.stdout.split("\n").filter(item => item.length > 0 && item != ".")

      const patches = new Map()
      const totalPatchesCountRes = await exec(`(cd ${repoLocation}; darcs log --count)`)
      const totalPatchesCount = parseInt(totalPatchesCountRes.stdout)
      let hunkRegex = RegExp(/^ *hunk /)

      // Get 100 patches at a time and parse those
      for (let i = 1; i <= totalPatchesCount; i = i + 100) {
        let patchesSubset = await exec(`(cd ${repoLocation}; darcs log --index=${i}-${i+100} -v)`)
        patchesSubset = patchesSubset.stdout.split("\n")
        do {
          const nextPatchStart = patchesSubset.findIndex((line, index) => {
            return index > 0 && line.startsWith("patch ")
          })
          const currentPatch = patchesSubset.slice(0, nextPatchStart - 1)
          patchesSubset = patchesSubset.slice(nextPatchStart)

          const hash = currentPatch[0].replace(&quot;patch ", "")
          const author = currentPatch[1].replace("Author: ", "")
          const date = currentPatch[2].replace("Date: ", "").trim()
          const name = currentPatch[3]
          const diffStart = currentPatch.findIndex((line) => {
            return line.match(hunkRegex)
          })

          const diffs = getDiffsFromPatchText(currentPatch.slice(diffStart).map(str =&gt; str.trimStart()).join(&quot;\n"))
          patches.set(hash, {
            name,
            author,
            date,
            hash,
            diffs,
          })
        } while (patchesSubset.length > 1)
_data/repos.js:51
Before
    })

      const info = {
        files,
        patches,
      }

      return [repoName, info]
    }))
  
After

      const info = {
        files,
        patches: [...patches.values()],
      }

      return [repoName, info]
    }))
  }
eleventy.config.js:5
Before
import * as Diff from 'diff'
After
eleventy.config.js:7
Before
import _ from 'lodash'
After
import { getDiffsFromPatchText } from './helpers.js'
eleventy.config.js:44
Before
  })

  eleventyConfig.addAsyncFilter("getPatchContents", async( repo, patchHash) => {
    const res = await exec(`(cd ${darcsConfig.repos[repo].location}; darcs log -h ${patchHash} -v --machine-readable)`)
    // TODO: look for closing bracket more sensibly than this
    const closingBracket = res.stdout.indexOf("]")
    const justTheDiffs = res.stdout.slice(closingBracket + 2) // Start at closingBracket plus the whitespace after it
    const lines = justTheDiffs.split("\n")
    const hunks = []
    let previousHunk = -1
    lines.forEach((line, index) => {
      if (line.startsWith("hunk") || index === lines.length - 1) {
        if (previousHunk === -1) {
          previousHunk = index
          return
        }
        // get diff from previous hunk to this next one
        const lastHunk = lines.slice(previousHunk, index)
        let lastHunkBefore = lastHunk.filter(line => line.startsWith("-")).map(str => str.replace("-", "")).join("\n")
        let lastHunkAfter = lastHunk.filter(line => line.startsWith("+")).map(str => str.replace("+", "")).join("\n")
        lastHunkBefore = _.escape(lastHunkBefore)
        lastHunkAfter = _.escape(lastHunkAfter)
        const changeObject = Diff.diffWords(lastHunkBefore, lastHunkAfter)
        changeObject.forEach((obj) => {
          if (obj.added) {
            lastHunkAfter = lastHunkAfter.replace(obj.value, `<span class=green>${obj.value}</span>`)
          }
          if (obj.removed) {
            lastHunkBefore = lastHunkBefore.replace(obj.value, `<span class=red>${obj.value}</span>`)
          }
        })

        let filename = lines[previousHunk].replace("hunk ./", "")
        const regex = RegExp(/(.*) ([0-9]+)$/)
        const matches = filename.match(regex)

        hunks.push({
          file: `${matches[1]}:${matches[2]}`,
          before: lastHunkBefore,
          after: lastHunkAfter,
          changeObject,
        })

        previousHunk = index
      }
    })

    return hunks
After
patch.njk:6
Before
permalink: "repos/{{patchInfo.repoName | slugify}}/patches/{{patchInfo.patch.details.hash}}/"
After
permalink: "repos/{{patchInfo.repoName | slugify}}/patches/{{patchInfo.patch.hash}}/"
patch.njk:10
Before
<p>{{patchInfo.patch.details.author}}</p>
<p>{{patchInfo.patch.details.hash}}</p>
{% set patchHunks = patchInfo.repoName | getPatchContents(patchInfo.patch.details.hash) %}
After
<p>{{patchInfo.patch.author}}</p>
<p>{{patchInfo.patch.hash}}</p>
{% set patchHunks = patchInfo.patch.diffs %}
patches.njk:12
Before
    <li><span><a href="/repos/{{repo | slugify}}/patches/{{patch.details.hash}}">{{patch.details.hash}}></a></span> - <span>{{patch.details.date | date}}</span> - <span>{{patch.details.author}}</span> - <span>{{patch.name}}</span></li>
After
    <li><span><a href="/repos/{{repo | slugify}}/patches/{{patch.hash}}">{{patch.hash}}></a></span> - <span>{{patch.date | date}}</span> - <span>{{patch.author}}</span> - <span>{{patch.name}}</span></li>