Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions .claude/skills/refresh-patch-stack/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
name: refresh-patch-stack
description: Refresh this fork's patch stack against upstream r-lib/cpp11 — merge the mirrored `main` into every patch branch, resolve conflicts, retire patches upstream has superseded, and rebuild `fork`. Use when asked to refresh, sync, or update the patch stack, or when a scheduled routine fires for it.
---

# Refresh the patch stack

This fork is a patch stack on top of
[`r-lib/cpp11`](https://github.com/r-lib/cpp11).
This skill brings it up to date with upstream.

Read [`.github/PATCHSTACK.md`](../../../.github/PATCHSTACK.md) first
if you are not already familiar with the branch layout.

## Layout

- `main` is a 1:1 mirror of upstream, hard-reset by the Pull app.
Never commit to it, and never merge into it.
- `a-*`, `b-*`, `f-*` are the patch branches,
one logical change each, based on `main`.
- `fork` is the default branch:
`main` plus every patch, squashed in lexicographic branch order.
It is rebuilt from scratch here and must never be merged into by hand.
- A long-lived draft pull request from `fork` into `main` provides CI.
Do not close it;
pushing `fork` is what re-runs the checks.

## When there is nothing to do

Fetch first, then compare:

```bash
git fetch --prune origin
git fetch --prune upstream # https://github.com/r-lib/cpp11.git
git rev-parse origin/main upstream/main
```

If `origin/main` has not moved since `patchstack/upstream-base`,
and no patch branch has changed,
stop and report that the stack is current.
Do not rebuild `fork` just to produce a commit.

If `origin/main` still lags `upstream/main`,
the Pull app has not run yet.
Say so and stop:
mirroring is the app's job, not this skill's.

## Refreshing a patch branch

For each `a-*`, `b-*`, `f-*` branch, **merge** `origin/main` into it.
Merge rather than rebase:
the merge commit is where the conflict resolution is recorded,
and it is what the next refresh builds on.

```bash
git checkout -B "$branch" "origin/$branch"
git merge --no-ff origin/main
```

A clean merge that leaves the branch's net effect unchanged
needs no further thought — push it.

### When it conflicts

Resolve toward **the fork's intent on upstream's new shape**.
Upstream reformats (Air for R, clang-format for C++)
and restructures;
re-apply the patch on top of that rather than reverting it.

Never resolve a conflict by taking the fork's whole file back.
That silently drops upstream's work in the same file.

### When a patch has been superseded

Upstream sometimes implements a patch's feature itself,
or fixes the bug it worked around.
Then the resolution **is the removal of the feature**:
take upstream's version of every file the patch touched,
so the branch's tree ends up equal to `origin/main`'s.

Say so explicitly in the merge commit message,
and say *why* — which upstream pull request or issue supersedes it,
and how you established that.
A patch retired without that reasoning
is indistinguishable from one dropped by accident.

Two signals worth checking before concluding a patch still earns its place:

- the upstream pull request that carries it, if there is one,
may have been closed;
- upstream's `NEWS.md` may cite the issue the patch addresses.

An empty branch is not deleted here.
Leave it;
the next patchstack sync detects it, deletes it,
and records the disposition in `refs/notes/patchstack`.

## Verifying

Before pushing anything, from the rebuilt `fork`:

```bash
air format . && git diff --exit-code
clang-format --dry-run -Werror $(git diff --name-only origin/main -- '*.hpp' '*.cpp')
Rscript -e 'devtools::load_all(quiet = TRUE); testthat::test_dir("tests/testthat", reporter = "summary")'
Rscript -e 'devtools::install(quiet = TRUE, upgrade = FALSE)'
Rscript -e 'devtools::clean_dll("./cpp11test"); devtools::test("./cpp11test", reporter = "summary")'
```

The C++ suite takes several minutes and is the one that matters most:
most of the stack is header changes,
and the R suite does not compile them.

Report a failure rather than working around it.
A red suite means the refresh stops and a human looks at it;
it does not mean the offending patch gets dropped.

## Rebuilding `fork`

`fork` is `main` plus each patch's net change,
applied in lexicographic branch order:

```bash
git checkout -B fork origin/main
git diff origin/main "origin/$branch" | git apply --index - # per branch, in order
git commit -m "<the patch's subject>"
```

Skip a branch whose net change against `origin/main` is empty.

The patches are expected to commute —
each applies to `main` on its own,
and the order changes nothing.
If applying one fails,
two patches have started to overlap:
report that rather than forcing it through,
because the squashed result would then depend on ordering
and stop being reproducible.

## Publishing

Push the patch branches and `fork` together:

```bash
git push --atomic --force-with-lease origin <each changed branch> fork
```

`--atomic` so a stale lease leaves every branch as it was
rather than half a stack.
`--force-with-lease` because `fork` is rebuilt
and a refreshed patch branch may be rewritten.

## Reporting

Say, briefly:

- which upstream commits arrived;
- for each patch: clean, resolved (and how), superseded (and why), or failed;
- the verification results;
- anything a human needs to decide.

If every patch merged cleanly and the suites passed,
that is a short report.
Keep it short.
96 changes: 96 additions & 0 deletions .github/PATCHSTACK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# The patch stack

This is a fork of [r-lib/cpp11](https://github.com/r-lib/cpp11) that carries a
handful of changes upstream does not ship.
It is maintained as a *patch stack*:
one branch per change,
each a single commit on top of upstream,
and one branch that is upstream plus all of them.

## The branches

- **`main`** is a 1:1 mirror of `r-lib/cpp11`'s `main`.
The [Pull app](https://pull.git.ci/) hard-resets it on every sync
([`pull.yml`](pull.yml)),
so a commit made here is discarded the next time upstream moves.
Never commit to it.
- **`fork`** is the default branch and the one to install:
`main` plus every patch, squashed in lexicographic branch order.
It is rebuilt from scratch on each sync — never commit to it either,
and never merge into it.
- **`a-fork-infra`**, **`b-*`**, **`f-*`** are the patch branches,
each based on `main`.
`a-` is this fork's own infrastructure (it sorts first, so it lands first),
`b-` is a bug fix, `f-` is a feature.
A working branch under any other name is left alone.

## CI

A long-lived **draft pull request from `fork` into `main`** runs the checks.
Upstream's `R-CMD-check` triggers on `pull_request` against `main`,
so the stack is checked without this fork editing a workflow file —
which also keeps `.github/workflows/` identical to upstream's
and out of the rename surface.

Do not close that pull request and do not merge it.
Merging would put the stack on `main`,
and the Pull app would discard it on the next mirror.
Pushing `fork` is what re-runs the checks.

## The refresh

[`.claude/skills/refresh-patch-stack/`](/.claude/skills/refresh-patch-stack/SKILL.md)
is a Claude skill that merges the mirrored `main` into every patch branch,
resolves the conflicts, retires patches upstream has superseded,
rebuilds `fork`, and pushes.
A scheduled routine invokes it;
it can also be run by hand.

It is a skill rather than a GitHub Actions workflow for two reasons.
Conflict resolution needs judgement —
upstream reformats and restructures,
and re-applying a patch on the new shape is not a mechanical merge.
And a workflow that rebuilds `fork` would have to push
`.github/workflows/` back to the branch,
which `GITHUB_TOKEN` may not do,
so it would need a personal access token to exist at all.

A patch that cannot be resolved is reported and left alone,
so one stuck patch never blocks the others.
A patch whose change has landed upstream ends up empty;
the branch is deleted and the disposition recorded
in `refs/notes/patchstack`.

## Adding a patch

```bash
git fetch origin
git switch -c f-my-change origin/main
# ... one commit ...
git push -u origin f-my-change
```

The next sync folds it into `fork`.
Keep it to a single commit where you can:
the stack is easier to read,
and the squash into `fork` is what everyone consumes anyway.

Each patch is a candidate for upstream.
Opening a pull request from its branch against `r-lib/cpp11`
costs nothing here — the branch stays exactly where the sync expects it —
and a merged patch cleans itself up on the following run.

## Installing

```r
pak::pak("krlmlr/cpp11")
```

`fork` is the default branch, so this installs the whole stack.
It is also built by
[krlmlr.r-universe.dev](https://krlmlr.r-universe.dev),
which is the faster route:

```r
install.packages("cpp11", repos = c("https://krlmlr.r-universe.dev", getOption("repos")))
```
26 changes: 26 additions & 0 deletions .github/pull.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Configuration for the "Pull" GitHub App, which keeps this fork's `main` in sync
# with the upstream r-lib/cpp11 repository.
#
# Pointers:
# - App / install / manage: https://pull.git.ci/
# - Source: https://github.com/wei/pull
# - Configuration reference: https://github.com/wei/pull/blob/master/docs/CONFIGURATIONS.md
# - Trigger a manual sync: https://pull.git.ci/process/krlmlr/cpp11
#
# Notes:
# - This file must live on the fork's default branch (`fork`) for Pull to read it.
# - `main` is a 1:1 mirror of r-lib/cpp11's `main`, so `hardreset` is the only
# correct merge method: any commit made on it here is discarded on the next sync.
# Nothing may be committed to `main` -- the patch branches carry this fork's work.
# - `fork` has no rule on purpose: it is not a mirror but `main` plus the squashed
# patch stack, and .github/workflows/patchstack-sync.yml rebuilds it once Pull
# has moved `main` forward.
# - Deleting this file would not disable Pull: without a configuration it hard-resets
# the fork's default branch from upstream, which is exactly what `fork` must not do.

version: "1"

rules:
- base: main
upstream: r-lib:main
mergeMethod: hardreset
23 changes: 17 additions & 6 deletions R/register.R
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ cpp_register <- function(
funs <- get_registered_functions(all_decorations, "cpp11::register", quiet)

package <- desc::desc_get("Package", file = file.path(path, "DESCRIPTION"))
package <- sub("[.]", "_", package)
package <- gsub("[.]", "_", package)

cpp_functions_definitions <- generate_cpp_functions(funs, package)

Expand Down Expand Up @@ -114,17 +114,28 @@ cpp_register <- function(
)
}

pkg_types_name <- paste0(package, c("_types.h", "_types.hpp"))

pkg_types <- c(
file.path(path, "src", paste0(package, "_types.h")),
file.path(path, "src", paste0(package, "_types.hpp")),
file.path(path, "inst", "include", paste0(package, "_types.h")),
file.path(path, "inst", "include", paste0(package, "_types.hpp"))
file.path(path, "src", pkg_types_name),
file.path(path, "src", "include", pkg_types_name),
file.path(path, "inst", "include", pkg_types_name)
)

# `src/cpp11.cpp` is generated next to the `src/` copies and compiled from
# `src/`, so a header in `src/include/` is included through that directory.
# A package keeping its own headers private this way then needs no include
# flag for them, and none of the other two locations changes.
pkg_types_include <- c(
pkg_types_name,
file.path("include", pkg_types_name),
pkg_types_name
)

pkg_types_exist <- file.exists(pkg_types)
if (any(pkg_types_exist)) {
extra_includes <- c(
sprintf('#include "%s"', basename(pkg_types[pkg_types_exist])),
sprintf('#include "%s"', pkg_types_include[pkg_types_exist]),
extra_includes
)
}
Expand Down
Loading