Commit 5e986532 authored by Dan Allen's avatar Dan Allen
Browse files

merge !1092

resolves #1187 use . to refer to current worktree and /. to refer to main worktree
parents c7327b58 3a1b01f4
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -9,6 +9,7 @@ This project utilizes semantic versioning.

=== Changed

* *content-aggregator*: Use symbolic name `.` to refer to current worktree and `/.` to refer to main worktree on `worktrees` key (#1187)
* *content-aggregator*: Allow symbolic name `.` to be placed anywhere in list of patterns on `worktrees` key (#1188)
* *content-classifier*: Allow page alias to resolve to an attachment (#1185)
* *asciidoc-loader*: Store document ID, if set, on asciidoc metadata object on page (#1186)
+1 −1
Original line number Diff line number Diff line
@@ -108,7 +108,7 @@ Here's how the branches are selected in author mode:
Aside from its name, the current branch is effectively ignored.
* The current branch of your worktree must match the `branches` filter configured on the `url` entry.
If not, the working tree will be ignored.
You can use the xref:content-branches.adoc#current-local-branch[reserved HEAD value] to ensure the current working tree is always used.
You can use the xref:content-branches.adoc#current-worktree[reserved HEAD value] to ensure the current working tree is always used.

If you want to use multiple worktrees, simply clone the repository multiple times and configure multiple entries in the playbook.
You can use the `branches` key to xref:content-branches.adoc[filter out the names of branches you don't want].
+70 −2
Original line number Diff line number Diff line
@@ -157,8 +157,8 @@ content:
We recommend against using this inverted selection since it can pull in branches you probably don't want.
It's best to be specific about the branches you want to match, then use exclusions to reduce that list.

[#current-local-branch]
== Use the current, local branch
[#current-worktree]
== Use the current worktree and branch

When working with a local repository, you may find yourself switching between branches often.
To save you from having to remember to update the playbook file to point to the current branch, you can use the reserved name `HEAD` for this purpose.
@@ -172,3 +172,71 @@ content:
----

The value `HEAD` is equivalent to using the name of the current branch.

[#named-worktree]
== Use the current branch of a worktree

If you're working with multiple branches of content, and you find yourself switching between those branches often in the main worktree, you may consider using linked worktrees.
A linked worktree is an additional worktree for a git repository that exists alongside other worktrees, including the main worktree.

In the past, we've said that the branches value `HEAD` refers to the main worktree.
However, this isn't the whole picture.
In actuality, `HEAD` refers to the worktree at the location specified by the content source `url` key, not necessary the main worktree.
If you're using linked worktrees (alongside the main worktree) to manage multiple branches, the `HEAD` keyword alone is not enough to differentiate between available worktrees.
Clearly, when using multiple worktrees, we need a way to be able to specify which `HEAD` we're talking about.

Antora introduces a special branch syntax you can use to select different worktrees using the branches filter.
To refer to a specific worktree, you use the syntax `HEAD@<name>`, where `<name>` is the name (or symbolic name) of the worktree.
The available worktrees to select are controlled by the `worktrees` key.

For examaple, to use the files in the linked worktree named v1.0.x, you use `[email protected]`.
You can also use a glob pattern to refer to multiple linked worktrees (e.g., `HEAD@*`).

Let's look at the configuration in the playbook to use the worktree named `1.0.x`.

[,yaml]
----
content:
  sources:
  - url: workspace/my-content-source
    branches: [email protected]
    worktrees: v1.0.x
----

First, we make the v1.0.x worktree available using the `worktrees` key, then we select that worktree using the `branches` key.

In addition to worktree names, there are two symbolic worktree names you can use, `.` and `/.`.

`.`:: Refers to the worktree at the directory specified by the `url` key.
That means that `HEAD` is effectively an alias of `HEAD@`, so using `HEAD` is sufficient.
+
Let's look at an example where the content source URL is a linked worktree.
+
[,yaml]
----
content:
  sources:
  - url: workspace/my-content-source-1.0.x
    branches: HEAD
----
+
We don't have to specify the worktrees key in this case since Antora automatically selects the current worktree (e.g., `.`).

`/.`:: Refers to the worktree at the root of the repository (i.e., the main worktree).
This syntax is only needed when you're using, and starting from, a linked worktree.
Using `HEAD@/.` tells Antora to use the files in the main worktree, even when the content source points to a linked worktree of that repository.
+
Let's look at that previous example, but select the main worktree instead.
+
[,yaml]
----
content:
  sources:
  - url: workspace/my-content-source-1.0.x
    branches: HEAD@/.
    worktrees: true
----
+
Notice that, this time, we need to set `worktrees: true` to make all available worktrees, including the main worktree, selectable by the branches pattern.
(Setting `worktrees: /.` would have worked as well).
We expect that the version of the content used will be version specified on the main branch, not the v1.0.x branch.
+11 −4
Original line number Diff line number Diff line
@@ -22,7 +22,7 @@ So we can say that worktrees are used by association when their branch is select

There are a few things to note about worktrees.
Typically, a worktree has the same name as the branch from which it was created, but not always.
The main worktree does not have a formal name and is therefore represented in Antora by the symbolic name `.` (to symbolize the current directory of the content source).
The main worktree does not have a formal name and is therefore represented in Antora by the symbolic name `/.` (to symbolize the current directory at the main repository root).

NOTE: The main worktree is not necessarily linked to the main branch.
The fact that they are both use the term "`main`" is merely coincidental.
@@ -63,6 +63,8 @@ Antora supports using a linked worktree directly as a content source.
If you don't specify the worktrees key, Antora automatically selects the current (linked) worktree.
You can then use the files in that worktree by using `HEAD` as the value of the `branches` key.

If you start from a linked worktree, and want to use the main worktree, then you can use the symbolic name `/.` to refer to the main worktree.

== Don't use any worktrees

If you want Antora to bypass all worktrees, set the value of the `worktrees` key to the keyword `false`.
@@ -84,18 +86,23 @@ The `worktrees` key accepts the following keyword values:

true:: Use all worktrees (the main worktree and all linked worktrees).
false (or ~):: Do not use any worktrees (not the main worktree or any linked worktrees).
.:: Use only the main worktree. (default)
/.:: Use only the main worktree.
.:: Use only the current worktree. (default)
*:: Use only the linked worktrees.

The boolean values `true` and `false` can only be used as a singular value, not as an entry in the list.
The symbolic name `.` cannot be negated.
The symbolic names `.` and `/.` cannot be negated.

If your repository only has a main worktree, then `.` and `/.` act the same.
If the content source points to a linked worktree, then `.` matches that worktree and `/.` must be used to match the main worktree.

== Specify worktrees by glob pattern

If you want more fine-grained control over which worktrees Antora uses, you can specify a list of glob patterns.
You refer to worktrees by their name, which is often the branch name to which they are linked (but not always).
Thus, the glob pattern works the same as described on the xref:content-branches.adoc[] page.
If you want to refer to the main worktree, you do so using the `.` keyword.
If you want to refer to the current worktree, you do so using the `.` keyword.
If you want to refer to the main worktree, you do so using the `/.` keyword.

Let's configure Antora to use the main worktree as well as the linked worktree for the v2.0 branch.
The files for the v1.0 branch will be read from the git tree, even if there is a linked worktree associated with that branch.
+89 −58
Original line number Diff line number Diff line
@@ -293,11 +293,12 @@ async function selectStartPathsForRepository (repo, sources) {

// QUESTION should we resolve HEAD to a ref eagerly to avoid having to do a match on it?
async function selectReferences (source, repo, remote) {
  let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns } = source
  let { branches: branchPatterns, tags: tagPatterns, worktrees: worktreePatterns = '.' } = source
  const managed = 'url' in repo
  const isBare = managed || repo.noCheckout
  const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
  const noWorktree = managed ? undefined : false
  const isLinkedWorktree = repo.worktreeName // perhaps switch to worktree property ({ name, dir}) in future
  const refs = new Map()
  if (
    tagPatterns &&
@@ -321,29 +322,41 @@ async function selectReferences (source, repo, remote) {
  ) {
    return [...refs.values()]
  }
  const worktreeName = repo.worktreeName // possibly switch to worktree property ({ name, dir}) in future
  if (worktreeName) branchPatterns = branchPatterns.map((it) => (it === 'HEAD' ? 'HEAD@' + worktreeName : it))
  let usePrimaryWorktree
  if (worktreePatterns) {
  let useWorktree = false
  if (!managed && (useWorktree = {})) {
    if (worktreePatterns === '.') {
      worktreePatterns = (usePrimaryWorktree = true) && []
      isLinkedWorktree ? (useWorktree.linked = isLinkedWorktree) : isBare || (useWorktree.main = true)
      worktreePatterns = []
    } else if (!worktreePatterns) {
      worktreePatterns = []
    } else if (worktreePatterns === true) {
      worktreePatterns = (usePrimaryWorktree = true) && ['*']
      if (!isBare) useWorktree.main = true
      // NOTE if we don't start at a linked worktree, linked worktree cannot be current worktree
      if (isLinkedWorktree) useWorktree.linked = isLinkedWorktree
      worktreePatterns = ['*']
    } else if (worktreePatterns === '/.') {
      if (!isBare) useWorktree.main = true
      worktreePatterns = []
    } else {
      worktreePatterns = Array.isArray(worktreePatterns)
      worktreePatterns = (
        Array.isArray(worktreePatterns)
          ? worktreePatterns.map((pattern) => String(pattern))
          : splitRefPatterns(String(worktreePatterns))
      if (~worktreePatterns.indexOf('.')) {
        worktreePatterns = (usePrimaryWorktree = true) && worktreePatterns.filter((it) => it !== '.')
      ).reduce((accum, it) => {
        if (it === '/.') return (isBare || (useWorktree.main = true)) && accum
        if (it === '.') {
          isLinkedWorktree ? (useWorktree.linked = isLinkedWorktree) : isBare || (useWorktree.main = true)
        } else {
          accum.push(it)
        }
      if (worktreeName) worktreePatterns = worktreePatterns.map((it) => (it === '@' ? worktreeName : it))
        return accum
      }, [])
    }
  } else if (worktreePatterns === undefined) {
    worktreePatterns = worktreeName ? [worktreeName] : (usePrimaryWorktree = true) && []
  } else {
    worktreePatterns = []
    if (!(useWorktree.main || useWorktree.linked)) useWorktree = false
  }
  let currentBranch
  if (!isLinkedWorktree) {
    let headBranchIdx
    if (branchPatterns.length === 1 && (branchPatterns[0] === 'HEAD' || branchPatterns[0] === '.')) {
      if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
        branchPatterns = [currentBranch]
@@ -351,14 +364,12 @@ async function selectReferences (source, repo, remote) {
        return [...refs.values()]
      } else {
        // NOTE current branch is undefined when HEAD is detached
      const head = usePrimaryWorktree ? repo.dir : noWorktree
        const head = useWorktree.main ? repo.dir : noWorktree
        refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
        return [...refs.values()]
      }
  } else {
    let headBranchIdx
    } else if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
      // NOTE we can assume at least two entries if HEAD or . are present
    if (~(headBranchIdx = branchPatterns.indexOf('HEAD')) || ~(headBranchIdx = branchPatterns.indexOf('.'))) {
      if ((currentBranch = await getCurrentBranchName(repo, remote).then((branch) => branch ?? false))) {
        if (~branchPatterns.indexOf(currentBranch)) {
          branchPatterns.splice(headBranchIdx, 1)
@@ -368,7 +379,7 @@ async function selectReferences (source, repo, remote) {
      } else if (isBare) {
        branchPatterns.splice(headBranchIdx, 1)
      } else {
        const head = usePrimaryWorktree ? repo.dir : noWorktree
        const head = useWorktree.main ? repo.dir : noWorktree
        // NOTE current branch is undefined when HEAD is detached
        refs.set('HEAD', { shortname: 'HEAD', fullname: 'HEAD', type: 'branch', detached: true, head })
        branchPatterns.splice(headBranchIdx, 1)
@@ -391,17 +402,28 @@ async function selectReferences (source, repo, remote) {
      if (currentBranch != null) return [currentBranch]
      return getCurrentBranchName(repo).then((branch) => (branch ? [branch] : []))
    })
    const worktrees = await findWorktrees(repo, worktreePatterns, usePrimaryWorktree)
    const worktrees = await findWorktrees(repo, worktreePatterns, useWorktree)
    let onMatch
    if ((usePrimaryWorktree || worktreePatterns.length) && worktrees.size) {
      const symbolicNames = new Map()
      worktrees.forEach(({ name, symbolicName = 'HEAD@' + name }, shortname) => {
        localBranches.push(symbolicName)
        symbolicNames.set(symbolicName, shortname)
    if ((useWorktree || worktreePatterns.length) && worktrees.size) {
      const headNames = new Map()
      worktrees.forEach(({ name, symbolicNames }, shortname) => {
        if (name) {
          const headName = 'HEAD@' + name
          localBranches.push(headName)
          headNames.set(headName, shortname)
        }
        if (symbolicNames) {
          for (const symbolicName of symbolicNames) {
            const symbolicHeadName = symbolicName === 'HEAD' ? symbolicName : 'HEAD@' + symbolicName
            localBranches.push(symbolicHeadName)
            headNames.set(symbolicHeadName, shortname)
          }
        }
      })
      onMatch = (candidate, { pattern }) => {
        const shortname = symbolicNames.get(candidate)
        return shortname ? (pattern.startsWith('HEAD@') ? shortname : undefined) : candidate
        const shortname = headNames.get(candidate)
        if (!shortname) return candidate
        if (pattern === 'HEAD' || pattern.startsWith('HEAD@')) return shortname
      }
    }
    if (localBranches.length) {
@@ -1069,34 +1091,43 @@ function resolveRepositoryFromWorktree (repo) {
    )
}

function findWorktrees (repo, patterns, usePrimaryWorktree) {
  if (!(usePrimaryWorktree || patterns.length)) return new Map()
  const primaryWorktree =
    usePrimaryWorktree && !repo.noCheckout
      ? getCurrentBranchName(repo).then((branch) => branch && [branch, { head: repo.dir, name: '.' }])
function findWorktrees (repo, patterns, useWorktree) {
  const useLinkedWorktree = !!useWorktree.linked
  const mainWorktree = useWorktree.main
    ? getCurrentBranchName(repo).then((branch) => {
        if (!branch) return
        return [branch, { head: repo.dir, name: undefined, symbolicNames: useLinkedWorktree ? ['/.'] : ['/.', '.'] }]
      })
    : Promise.resolve()
  if (!patterns.length) return primaryWorktree.then((entry) => new Map(entry && [entry]))
  if (!(useLinkedWorktree || patterns.length)) return mainWorktree.then((entry) => new Map(entry && [entry]))
  const worktreesDir = ospath.join(repo.dir, repo.dir === repo.gitdir ? '' : '.git', 'worktrees')
  const patternCache = repo.cache[REF_PATTERN_CACHE_KEY]
  return fsp
  const scanWorktrees = patterns.length
    ? fsp
        .readdir(worktreesDir)
        .then((worktreeNames) => filterRefs(worktreeNames, patterns, patternCache), invariably.emptyArray)
        .then((worktreeNames) => {
          if (useLinkedWorktree && !~worktreeNames.indexOf(useWorktree.linked)) worktreeNames.push(useWorktree.linked)
          return worktreeNames
        })
    : Promise.resolve(useLinkedWorktree ? [useWorktree.linked] : [])
  return scanWorktrees
    .then((worktreeNames) =>
      Promise.all(
        worktreeNames.map((worktreeName) => {
          const gitdir = ospath.resolve(worktreesDir, worktreeName)
        worktreeNames.map((name) => {
          const symbolicNames = useLinkedWorktree && name === useWorktree.linked ? ['.', 'HEAD'] : undefined
          const gitdir = ospath.resolve(worktreesDir, name)
          // NOTE branch name defaults to worktree name if HEAD is detached
          return getCurrentBranchName(Object.assign({}, repo, { gitdir })).then((branch = worktreeName) =>
          return getCurrentBranchName(Object.assign({}, repo, { gitdir })).then((branch = name) =>
            fsp
              .readFile(ospath.join(gitdir, 'gitdir'), 'utf8')
              .then((contents) => [branch, { head: ospath.dirname(contents.trimEnd()), name: worktreeName }])
              .then((contents) => [branch, { head: ospath.dirname(contents.trimEnd()), name, symbolicNames }])
          )
        })
      )
    )
    .then((entries) =>
      primaryWorktree.then((primary) => (primary ? new Map(entries).set(primary[0], primary[1]) : new Map(entries)))
    )
    .then((entries) => new Map(entries))
    .then((worktrees) => mainWorktree.then((result) => (result ? worktrees.set(result[0], result[1]) : worktrees)))
}

async function gracefulPromiseAllWithLimit (tasks, limit = Infinity) {
Loading