Branch

Remove darcs functionality :(

Sun Oct 12 2025

Tucker McKnight <tucker@pangolin.lan>

I'd like to come back to darcs later, when I've come up with a
way of making some kind of plugin system for making this work
with multiple VCSes.

In the meantime, development will be faster if I just focus
on git.

2376512fc3745b47f8cb9446f92c85d3fe25488b

Side-by-side
Stacked
frontend/main.ts:62
Before
62 63 64
    const isDarcs = event.target.dataset.vcs === "darcs"
    const copiedPrefix = isDarcs ? `darcs pull ${jsVars.baseUrl} -h ` : ""
    navigator.clipboard.writeText(`${copiedPrefix}${hash}`).then(() => {
After
62
    navigator.clipboard.writeText(hash).then(() => {
main.ts:55
Before
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
        if (repoConfig._type === "darcs") {
          for (let branch in repoConfig.branches) {
            const repoPath = eleventyConfig.dir.output + reposPath + "/" + eleventyConfig.getFilter("slugify")(repoName) + "/branches/" + branch
            const originalLocation = cwd + "/" + repoConfig.branches[branch].location
            // If it is there, do darcs pull
            if (fsImport.existsSync(repoPath + "/_darcs")) {
              await exec(`(cd ${repoPath} && darcs pull --no-interactive)`)
            } else {
              // If it is not there, do darcs clone
              await exec(`(cd ${repoPath} && mkdir -p temp; darcs clone ${originalLocation} temp/${branch} --no-working-dir; mv temp/${branch}/_darcs .; rm -R temp)`)
            }
          }
        } else if (repoConfig._type === "git") {
          const repoPath = eleventyConfig.dir.output + reposPath + "/" + eleventyConfig.getFilter("slugify")(repoName)
          const gitRepoName = eleventyConfig.getFilter("slugify")(repoName) + ".git"
          // If it is there, do git pull
          if (fsImport.existsSync(repoPath + ".git")) {
            // git repos are just in the repos folder, not in their subdir
            // create string of commands saying 'git fetch origin branch:branch' for each branch
            const fetchCommands = repoConfig.branchesToPull.map(branch => `git fetch origin ${branch}:${branch}`).join('; ')
            await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} && ${fetchCommands}; git update-server-info)`)
          } else {
            // If it is not there, do git clone
            const originalLocation = cwd + "/" + repoConfig.location
            await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/"} && git clone ${originalLocation} ${gitRepoName} --bare)`)
            await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} && git update-server-info)`)
          }
          if (typeof repoConfig.artifactSteps !== 'undefined') {
            // make a temp directory for things to run in
            const tempDirName = `temp_${Math.floor(Math.random() * 10000).toString()}`
            const tempDir = `${directories.output}${reposPath}${tempDirName}`
            const tempDirRepoPath = `${tempDir}/${eleventyConfig.getFilter("slugify")(repoName)}`
            const mkdirresult = await exec(`mkdir ${tempDir}`)
            await exec(`git clone -s ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}.git ${tempDirRepoPath}`)
            for (let branch of repoConfig.branchesToPull) {
              await exec(`(cd ${tempDirRepoPath} && git checkout ${branch})`)
              for (let artifactStep of repoConfig.artifactSteps) {
                // Run the command for each step in each branch
                await exec(`(cd ${tempDirRepoPath} && ${artifactStep.command})`)
                // Copy the specified folders from the "from" to the "to" dir
                await exec(`cp -r --remove-destination ${tempDirRepoPath}/${artifactStep.copyFrom} ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branch)}/${artifactStep.copyTo}`)
              }
            // delete the temp dirs
            await exec(`rm -r ${tempDir}`)
After
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
        const repoPath = eleventyConfig.dir.output + reposPath + "/" + eleventyConfig.getFilter("slugify")(repoName)
        const gitRepoName = eleventyConfig.getFilter("slugify")(repoName) + ".git"
        // If it is there, do git pull
        if (fsImport.existsSync(repoPath + ".git")) {
          // git repos are just in the repos folder, not in their subdir
          // create string of commands saying 'git fetch origin branch:branch' for each branch
          const fetchCommands = repoConfig.branchesToPull.map(branch => `git fetch origin ${branch}:${branch}`).join('; ')
          await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} && ${fetchCommands}; git update-server-info)`)
        } else {
          // If it is not there, do git clone
          const originalLocation = cwd + "/" + repoConfig.location
          await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/"} && git clone ${originalLocation} ${gitRepoName} --bare)`)
          await exec(`(cd ${eleventyConfig.dir.output + reposPath + "/" + gitRepoName} && git update-server-info)`)
        }
        if (typeof repoConfig.artifactSteps !== 'undefined') {
          // make a temp directory for things to run in
          const tempDirName = `temp_${Math.floor(Math.random() * 10000).toString()}`
          const tempDir = `${directories.output}${reposPath}${tempDirName}`
          const tempDirRepoPath = `${tempDir}/${eleventyConfig.getFilter("slugify")(repoName)}`
          const mkdirresult = await exec(`mkdir ${tempDir}`)
          await exec(`git clone -s ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}.git ${tempDirRepoPath}`)
          for (let branch of repoConfig.branchesToPull) {
            await exec(`(cd ${tempDirRepoPath} && git checkout ${branch})`)
            for (let artifactStep of repoConfig.artifactSteps) {
              // Run the command for each step in each branch
              await exec(`(cd ${tempDirRepoPath} && ${artifactStep.command})`)
              // Copy the specified folders from the "from" to the "to" dir
              await exec(`cp -r --remove-destination ${tempDirRepoPath}/${artifactStep.copyFrom} ${directories.output}${eleventyConfig.getFilter("slugify")(repoName)}/branches/${eleventyConfig.getFilter("slugify")(branch)}/${artifactStep.copyTo}`)
          // delete the temp dirs
          await exec(`rm -r ${tempDir}`)
main.ts:210
Before
210 211 212 213 214 215 216
    let command = ''
    if (config._type === "git") {
      command = `git show ${branch}:${filename}`
    }
    else if (config._type === "darcs") {
      command = `darcs show contents ${filename}`
    }
After
210
    const command = `git show ${branch}:${filename}`
main.ts:237
Before
237 238 239 240 241 242 243
    let command = ''
    if (config._type === "git") {
      command = `git show ${branchName}:README.md`
    }
    else if (config._type === "darcs") {
      command = `darcs show contents README.md`
    }
After
237
    const command = `git show ${branchName}:README.md`
partial_templates/main_top.njk:10
Before
10 11 12 13 14 15 16 17 18 19
        window.jsVars['cloneDiv'] = `{%- if reposConfig.repos[nav.repoName]._type == &quot;darcs" -%}{% set url = repos[nav.repoName].cloneUrl + (nav.branchName | slugify) %}
&lt;label class="form-label">HTTPS URL</label>
<div class="input-group d-flex flex-nowrap">
  <span class="clone overflow-hidden input-group-text">
    {{ url }}
  </span>
  <button data-clone-url="{{url}}" class="btn btn-primary" id="clone-button">Copy</button>
</div>{%- elif reposConfig.repos[nav.repoName]._type == "git" -%}<label class="form-label">HTTPS URL</label>
</div>
{%- endif -%}`;
After
10 11
        window.jsVars['cloneDiv'] = `<label class="form-label">HTTPS URL</label>
</div>`;
schemas/ReposConfiguration.json:2
Before
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
    "DarcsConfig": {
      "additionalProperties": false,
      "description": "A configuration object for a darcs repository.",
      "properties": {
        "_type": {
          "const": "darcs",
          "description": "Must be set to `\"darcs\"`.",
          "type": "string"
        },
        "artifactSteps": {
          "items": {
            "additionalProperties": false,
            "properties": {
              "command": {
                "type": "string"
              },
              "copyFrom": {
                "type": "string"
              },
              "copyTo": {
                "type": "string"
              }
            },
            "required": [
              "command",
              "copyFrom",
              "copyTo"
            ],
            "type": "object"
          },
          "type": "array"
        },
        "branches": {
          "additionalProperties": {
            "additionalProperties": false,
            "description": "Each key in this object will be the name that is used for that branch.",
            "properties": {
              "description": {
                "description": "A description of this branch. You may want to clarify what this branch is used for here.",
                "type": "string"
              },
              "location": {
                "description": "The absolute path to the repository for this branch.",
                "type": "string"
              }
            },
            "required": [
              "location"
            ],
            "type": "object"
          },
          "description": "The branches of this repository to generate pages for. Since darcs doesn't have branches in the same way that other VCSs do, a \"branch\" in this case is an entirely separate repository. So, these \"branches\" are more like repos that are grouped under the same project name. That is why you need to specify a separate location for each branch.",
          "type": "object"
        },
        "defaultBranch": {
          "description": "The name of the default branch. Should match one of the keys in  {@link  DarcsConfig.branches } .",
          "type": "string"
        },
        "languageExtensions": {
          "additionalProperties": {
            "type": "string"
          },
          "description": "If your repository has any uncommon file extensions that should be treated like a different type of file, list them here. If you include `{njk: \"html\"}` here, that will tell the syntax highlighter to highlight an `njk` file like an `html` file. The key is the file extension in your code, and the value is the file extension that the syntax highlighter will know about.",
          "type": "object"
        }
      },
      "required": [
        "_type",
        "defaultBranch",
        "branches"
      ],
      "type": "object"
    },
After
2
schemas/ReposConfiguration.json:137
Before
137
      "description": "The ReposConfiguration object contains information about your local repositories, like their name and location on your local filesystem. Add repositories to this configuration object to make a static site for them.\n\nThis static site generator works with both git and darcs repositories. Because of the differences between these two version control systems, the configuration for them looks a little different. Both types of configuration objects can be nested underneath the  {@link  ReposConfiguration.repos }  key.\n\nYou will also need to set the  {@link  ReposConfiguration.baseUrl }  to the URL of your live website.",
After
137
      "description": "The ReposConfiguration object contains information about your local repositories, like their name and location on your local filesystem. Add repositories to this configuration object to make a static site for them.\n\nYou will also need to set the  {@link  ReposConfiguration.baseUrl }  to the URL of your live website.",
schemas/ReposConfiguration.json:149
Before
149 150 151 152 153 154 155 156 157
            "anyOf": [
              {
                "$ref": "#/definitions/GitConfig"
              },
              {
                "$ref": "#/definitions/DarcsConfig"
              }
            ],
            "description": "An object containing the configuration for your repositories. Each key in this object is a repository name, and the value has several config options for that repository. The required config options describe the path to the repository and which branches should be pulled. See the specific definitions of  {@link  GitConfig }  and  {@link  DarcsConfig }  for more details about what goes in these configuration objects."
After
149 150
            "$ref": "#/definitions/GitConfig",
            "description": "An object containing the configuration for your repositories. Each key in this object is a repository name, and the value has several config options for that repository. The required config options describe the path to the repository and which branches should be pulled. See the specific definition of  {@link  GitConfig }  for more details about what goes in these configuration objects."
src/configTypes.ts:3
Before
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 * This static site generator works with both git and darcs repositories. Because of the
 * differences between these two version control systems, the configuration for them
 * looks a little different. Both types of configuration objects can be nested underneath
 * the {@link ReposConfiguration.repos} key.
 * 
 *     "My Darcs Project": {
 *       _type: "darcs",
 *       branches: {
 *         'main': {
 *           location: "/home/alice/projects/my_darcs_project",
 *           description: &quot;Main branch of this project."
 *         },
 *         &#39;drafts': {
 *           location: "/home/alice/projects/my_darcs_project_drafts/",
 *           description: "Some things that are a work in progress",
 *         },
 *       },
 *       languageExtensions: {
 *         "njk": "html",
 *       },
After
3 4 5
 *     "My Git Project": {
 *       _type: "git",
 *       branches: ['main', 'develop']
src/configTypes.ts:40
Before
40 41
    * definitions of {@link GitConfig} and {@link DarcsConfig} for more details
    [repoName: string]: GitConfig | DarcsConfig
After
40 41
    * definition of {@link GitConfig} for more details
    [repoName: string]: GitConfig
src/configTypes.ts:78
Before
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 156 157 158 159 160 161 162 163 164

/**
* A configuration object for a darcs repository.
*
* @example
* "My Darcs Project": {
*   _type: "darcs",
*   baseUrl: "https://repos.tuckerm.us",
*   defaultBranch: 'main',
*   branches: {
*     'main': {
*       location: "/home/alice/projects/my_darcs_project",
*       description: "Main branch of this project."
*     },
*     'drafts': {
*       location: "/home/alice/projects/my_darcs_project_drafts/",
*       description: "Some things that are a work in progress",
*     },
*   },
*   languageExtensions: {
*     "njk": "html",
*   },
* }
*/
export type DarcsConfig = {
  /** Must be set to `"darcs"`. */
  _type: "darcs",
  /**
  * The name of the default branch. Should match one of the keys in {@link DarcsConfig.branches}.
  * @example defaultBranch: "main"
  * */
  defaultBranch: string,
  /** The branches of this repository to generate pages for. Since darcs doesn't have
  * branches in the same way that other VCSs do, a "branch" in this case is an entirely
  * separate repository. So, these "branches" are more like repos that are grouped under
  * the same project name. That is why you need to specify a separate location for each
  * branch.
  * @example
  * branches: {
  *   main: {
  *     location: "/home/alice/projects/my-project/main",
  *     description: "The main release branch of this project."
  *   },
  *   drafts: {
  *     location: "/home/alice/projects/my-project/drafts",
  *     description: "Undeployed works-in-progress for this project."
  *   }
  * }
  */
  branches: {
    /**
    * Each key in this object will be the name that is used for that branch.
    */
    [branchName: string]: {
      /**
      * The absolute path to the repository for this branch.
      * @example location: "/home/alice/projects/darcs_repo_main"
      */
      location: string,
      /**
      * A description of this branch. You may want to clarify what this branch is used for here.
      */
      description?: string,
    }
  },
  /**
  * If your repository has any uncommon file extensions that should be treated like a different
  * type of file, list them here. If you include `{njk: "html"}` here, that will tell the
  * syntax highlighter to highlight an `njk` file like an `html` file. The key is the file
  * extension in your code, and the value is the file extension that the syntax highlighter
  * will know about.
  * @example
  * languageExtensions: {
  *   njk: "html",
  *   jss: "js",
  *   madeupformat: "txt"
  * }
  */
  languageExtensions?: {
    [fileExtension: string]: string
  },
  artifactSteps?: {
      command: string,
      copyFrom: string,
      copyTo: string,
  }[]
}
After
78
src/helpers.ts:85
Before
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
/** @hidden */
const getDarcsDiffsFromPatchText = (patchText: string): Array<DiffInfo> => {
  const lines = patchText.split("\n")
  const hunks: Array<Hunk> = []
  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 + 1) // slice is non-inclusive for the end argument
      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)
      let filename = lines[previousHunk].replace("hunk ./", "")
      const changeObject = Diff.diffWords(lastHunkBefore, lastHunkAfter)
      let previousText = ""
      let afterText = ""

      changeObject.forEach((obj) => {
        if (!obj.added && !obj.removed) {
          previousText = previousText + obj.value
          afterText = afterText + obj.value
        }
        if (obj.added) {
          afterText = afterText + "<mark>" + obj.value + "</mark>"
        }
        if (obj.removed) {
          previousText = previousText + "<mark>" + obj.value + "</mark>"
        }
      })
      const regex = RegExp(/(.*) ([0-9]+)$/)
      const matches = filename.match(regex)
      const file = matches[1]
      const lineNumber: number = parseInt(matches[2])

      hunks.push({
        file,
        lineNumber,
        previousText,
        afterText,
      })
      previousHunk = index
    }
  })
  return hunks
}

  if (config._type === "darcs") {
    return config.branches[branchName].location
  }
  else if (config._type === "git") {
    return config.location
  }
  getDarcsDiffsFromPatchText,
After
85
  return config.location
src/repos.ts:1
Before
1
  type DarcsConfig,
After
1
src/repos.ts:20
Before
20 21 22 23 24 25 26
const getBranchNames = (repoConfig: DarcsConfig | GitConfig): Array<string> => {
  if (repoConfig._type === 'darcs') {
    return Object.keys(repoConfig.branches)
  }
  else if (repoConfig._type === 'git') {
    return repoConfig.branchesToPull
  }
After
20 21
const getBranchNames = (repoConfig: GitConfig): Array<string> => {
  return repoConfig.branchesToPull
src/vcses/darcs/helpers.ts:1
Before
1 2 3 4 5
export default {
    cloneUrl: (baseUrl: string, repoName: string) => {
        return `${baseUrl}/${repoName.toLowerCase().replaceAll(" ", "-")}/branches/`
    }
}
After
1
src/vcses/darcs/operations.ts:1
Before
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
import util from 'util'
import childProcess from 'child_process'
const exec = util.promisify(childProcess.exec)
import {getDarcsDiffsFromPatchText} from '../../helpers.ts'

export const getFileList = async(repoName: string, branchName: string, repoLocation: string) => {
  const command = "darcs show files"

  const result = await exec(`(cd ${repoLocation} && ${command})`)
  const files = result.stdout.split("\n").filter(item => item.length > 0 && item != ".")
  return files
}

export const getBranchInfo = async (repoName: string, branchName: string, repoLocation: string) => {
  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 patchesSubsetRes = await exec(`(cd ${repoLocation} && darcs log --index=${i}-${i+100} -v)`)
    let patchesSubset = patchesSubsetRes.stdout.split("\n")
    do {
      const nextPatchStart = patchesSubset.findIndex((line, index) => {
        return (index > 0 && line.startsWith("patch ")) || index === patchesSubset.length - 1
      })
      const isEndOfFile = nextPatchStart === patchesSubset.length - 1
      const currentPatch = patchesSubset.slice(0, isEndOfFile ? nextPatchStart : nextPatchStart - 1)
      patchesSubset = patchesSubset.slice(nextPatchStart)
      const hash = currentPatch[0].replace("patch ", "").trim()
      const author = currentPatch[1].replace("Author: ", "").trim()
      const date = currentPatch[2].replace("Date: ", "").trim()
      const name = currentPatch[3].replace("  * ", "").trim()
      const diffStart = currentPatch.findIndex((line) => {
        return line.match(hunkRegex)
      })
      const description = currentPatch.slice(5, diffStart).map(str => str.replace("  ", "")).join("\n").trim()
      const diffs = getDarcsDiffsFromPatchText(currentPatch.slice(diffStart).map(str => str.trimStart()).join("\n"))
      patches.set(hash, {
        name,
        description,
        author,
        date,
        hash,
        diffs,
      })
    } while (patchesSubset.length > 1)
  }

  return Array.from(patches.values())
}

export const getFileLastTouchInfo = async (repoName: string, branchName: string, filename: string, repoLocation: string) => {
  const command = `darcs annotate --machine-readable ${filename}`
  const res = await exec(`(cd ${repoLocation} && ${command})`)
  const output = res.stdout
  const outputLines = output.split("\n").map((line) => {
    return line.split(' ')[0]
  })

  return outputLines.map((line) => {
    return {sha: line, author: ''}
  })
}
After
1
src/vcses/helpers.ts:1
Before
1 2
import darcsHelpers from './darcs/helpers.ts'
    darcs: darcsHelpers
After
1
src/vcses/operations.ts:3
Before
3 4 5 6 7
import {
  getBranchInfo as getDarcsBranchInfo,
  getFileList as getDarcsFileList,
  getFileLastTouchInfo as getDarcsFileLastTouchInfo,
} from './darcs/operations.ts'
After
3
src/vcses/operations.ts:24
Before
24 25 26 27 28
  darcs: {
    getBranchInfo: getDarcsBranchInfo,
    getFileList: getDarcsFileList,
    getFileLastTouchInfo: getDarcsFileLastTouchInfo,
  }
After
24
templates/patch.njk:10
Before
10 11 12 13 14 15 16 17 18
      {% if reposConfig.repos[patchInfo.repoName]._type == "darcs" %}
      <div class="input-group mb-3 d-flex flex-nowrap">
        <span id="clone-command" class="clone input-group-text overflow-hidden">
          {% set url = [reposConfig.baseUrl, reposPath, "/", patchInfo.repoName | slugify, "/branches/", patchInfo.branchName | slugify] | join | url %}
          darcs pull {{ url }} -h {{patchInfo.patch.hash}}
        </span>
        <button class="btn btn-primary" id="clone-button" onclick="copyCommand()">Copy</button>
      </div>
      {% endif %}
After
10
templates/repo.njk:24
Before
24 25 26 27 28 29
            {% if reposConfig.repos[branch.repoName]._type == "darcs" %}
            <button data-hash="{{patch.hash}}" data-vcs="darcs" class="copy-btn btn btn-sm btn-outline-primary ms-2">
              <i class="bi-copy bi me-1"></i>darcs pull {{patch.hash | truncate(6, true, "")}}
            </button>
            {% elif reposConfig.repos[branch.repoName]._type == "git" %}
            {% endif %}
After
24