Skip to content

Commit b44a40f

Browse files
authored
Merge branch 'phpstan:2.3.x' into 3579
2 parents 54c31af + ed978d4 commit b44a40f

4,527 files changed

Lines changed: 528838 additions & 38907 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
---
2+
name: rebase-branch
3+
description: Rebase the current feature branch onto the fresh tip of a base branch (e.g. 2.3.x) — read both sides first, plan every conflict, and keep turbo-shadowed PHP classes and their turbo-ext C++ mirrors identical in each replayed commit
4+
argument-hint: "[base-branch]"
5+
---
6+
7+
# Rebasing a feature branch onto a fresh base
8+
9+
The base branch is `$ARGUMENTS`. If that is empty, use the base of the branch's pull request
10+
(`gh pr view --json baseRefName -q .baseRefName`); if there is no pull request, ask.
11+
12+
Work in the checkout that has the feature branch checked out. Address it by absolute path
13+
(`git -C <path>`), because the shell's working directory resets between commands. Never use `git stash`:
14+
the stash is shared by every worktree of the repository. `git rebase -i` runs here only with a
15+
scripted `GIT_SEQUENCE_EDITOR` (step 4), never with an interactive editor.
16+
17+
Helpers in this skill's directory (the directory this SKILL.md was loaded from):
18+
19+
- `rebase-plan.php <new-base> <old-tip>` prints a read-only report for the planning step.
20+
- `rebase-todo.php` is a `GIT_SEQUENCE_EDITOR` that marks commits `drop` or `edit`.
21+
22+
If the feature branch is older than this skill, the helpers are not in its checkout. Extract them with
23+
`git show origin/2.3.x:.claude/skills/rebase-branch/<file> > <scratchpad>/<file>`.
24+
25+
## 1. Pin the three points
26+
27+
```bash
28+
git fetch origin
29+
git status --porcelain --untracked-files=no # must print nothing: commit first or ask
30+
BRANCH=$(git branch --show-current)
31+
OLD_TIP=$(git rev-parse HEAD)
32+
NEW_BASE=$(git rev-parse origin/<base>) # the remote tip, never the possibly stale local branch
33+
OLD_BASE=$(git merge-base "$NEW_BASE" "$OLD_TIP")
34+
git branch -f "backup/$BRANCH-pre-rebase" "$OLD_TIP"
35+
```
36+
37+
Write the three SHAs to the scratchpad. Every later step uses the SHAs, not refs that can move. If
38+
`OLD_BASE` equals `NEW_BASE`, stop, because there is nothing to rebase.
39+
40+
- **The feature's work** is `git diff $OLD_BASE $OLD_TIP`, commit by commit
41+
`git log --reverse -p $OLD_BASE..$OLD_TIP`.
42+
- **What happened on the base meanwhile** is `git diff $OLD_BASE $NEW_BASE`, commit by commit
43+
`git log --reverse --no-merges -p $OLD_BASE..$NEW_BASE`.
44+
45+
## 2. Read both sides, plan every conflict
46+
47+
Run `php <skill dir>/rebase-plan.php $NEW_BASE $OLD_TIP`. It reports:
48+
49+
- the base commits,
50+
- merge commits in the feature range,
51+
- feature commits the base already contains,
52+
- turbo bump commits,
53+
- files changed on both sides, with the feature commits that touch each one,
54+
- the conflicts that `git merge-tree` predicts for the two tips,
55+
- the turbo twin table (step 3),
56+
- names that vanished on one side but are still spelled in the other side's C++.
57+
58+
The report tells you where to look; it does not replace reading the changes. Read:
59+
60+
1. The feature's work: `git log --reverse --stat $OLD_BASE..$OLD_TIP`, then the diffs, starting with the
61+
files changed on both sides.
62+
2. The base's work: every base commit that touches a file changed on both sides, plus any base commit
63+
that renames, moves or changes the signature of code the feature uses. Those changes break the
64+
feature silently, without a textual conflict.
65+
3. For each file changed on both sides, put `git diff $OLD_BASE $NEW_BASE -- <file>` next to
66+
`git log --reverse -p $OLD_BASE..$OLD_TIP -- <file>`. Decide what the combined file must contain.
67+
68+
Then write the plan to the scratchpad. Include every feature commit that will conflict or needs a mirror
69+
port, each with the file, what the base did there, and the intended resolution. A resolution keeps the
70+
intent of both sides. When the feature reshaped code that the base then changed, carry the base's
71+
change into the feature's new shape; don't just pick one side. When the base moved code that the feature
72+
edits, apply the feature's edit at the new location. Where the right resolution is unclear, ask before
73+
you start, not in the middle of the replay.
74+
75+
Decide these before replaying (ask the user if the answer isn't obvious):
76+
77+
- **Merge commits in the feature range**: a rebase flattens them away. Linearize only with the user's
78+
agreement.
79+
- **Commits the base already contains**: this covers patch-identical commits and adapted twins, such as
80+
a chunk extracted into its own pull request that has since landed. Drop them instead of replaying
81+
them. Replaying one conflicts, and resolving it on the branch's side silently wipes out the base's
82+
version.
83+
- **`Bump expected turbo version` commits**: drop all of them and run one `make bump-turbo` at the end.
84+
During the replay, a conflict on `EXPECTED_EXTENSION_VERSION` resolves to the base's value.
85+
86+
## 3. Turbo mirrors
87+
88+
Classes carrying `#[ShadowedByTurboExtension(turboClass: ..., implementation: .../turbo-ext/src/X.cpp)]`
89+
have a native C++ twin. Vendored twins live in `VENDORED_PAIRS` in
90+
`build/PHPStan/Build/TurboAttributeCollector.php` and change when their package's version in
91+
`composer.lock` changes. The PHP class and the C++ class must behave identically after every replayed
92+
commit. A mirror change goes into the same commit as the PHP change it mirrors, never into a trailing
93+
"port" commit.
94+
95+
The report's twin table uses these classifications:
96+
97+
- **Shadowed at OLD BASE and still on both sides**: each side already mirrored its own PHP edits.
98+
Resolve a `.cpp` conflict the same way as the matching `.php` conflict, so the merged `.cpp` mirrors
99+
the merged `.php`. If a side changed the `.php` but not the `.cpp`, check whether that change needed a
100+
port (a docblock-only change does not).
101+
- **NEW ON BASE**: the base ported a class that the feature edits. Every feature commit that changes
102+
that PHP class must make the same change in the base's `.cpp`. Mark those commits `edit`.
103+
- **NEW ON FEATURE**: the feature ports a class that the base changed in the meantime. Port the base's
104+
PHP change into the `.cpp` in the feature commit that introduces the `.cpp`, and mark that commit
105+
`edit`. Later feature commits that touch the `.cpp` then replay on top of the port.
106+
- **SHADOWED ON BOTH SIDES INDEPENDENTLY**: reconcile the two ports by hand, and ask which one survives.
107+
- **UNSHADOWED on one side**: the other side's `.cpp` edits no longer apply. Resolve a modify/delete
108+
conflict by deleting the file.
109+
- **Names spelled in C++**: native code reaches PHP by name, which git cannot see. It calls methods and
110+
reads properties through method and property sites and string literals. It also copies constants
111+
(e.g. `PhpVersionFactory::MAX_PHP_VERSION` as `PT_MS_MAX_PHP_VERSION`) and slot numbers (a
112+
`PT_*_PROP_*` copy of another class's property layout). If a twin gains or loses a property on either
113+
side, grep turbo-ext for copies of its slot numbers. The report lists removed names and changed
114+
constant values it found spelled on the other side, but it can't see a changed parameter list.
115+
`side-by-side.php`, `signature-parity.php` and the tests catch that later.
116+
117+
The turbo machinery itself may differ between the base and the feature. Compare `turbo-ext/CLAUDE.md`
118+
and `turbo-ext/README.md` on both sides: stub shells vs real-name activation, handwritten `reg::Class`
119+
registrations vs generated `sig::` declarations, helper APIs in `reg.h`, `zv.h` and `support.h`. Write
120+
each mirror in the conventions of the commit being replayed. A port that is new on the base has to
121+
follow the feature's conventions from the feature commit that changed them onwards. The report's last
122+
section lists the shared turbo-ext files the feature changed.
123+
124+
Never hand-merge generated files. Regenerate them in the commit being replayed:
125+
126+
| File | Resolution |
127+
| --- | --- |
128+
| `turbo-ext/src/generated/*.h` | `php turbo-ext/bin/generate-declarations.php`, where the generator exists at that commit |
129+
| `turbo-ext/src/parser/ParserRunnerActions*.cpp`, `ParserRunnerActionsSplit.h` | `php turbo-ext/bin/generate-parser-actions.php` |
130+
| `composer.lock` | take the current (base) lock, re-apply the commit's `composer.json` change, then `composer update <the packages it changed>`, or `composer update --lock` when only the hash differs |
131+
| `phpstan-baseline.neon` | apply the replayed commit's own edit (`git show REBASE_HEAD -- phpstan-baseline.neon`) to the current file |
132+
| `src/Turbo/TurboExtensionEnabler.php` version | keep the base's value, bump at the end |
133+
134+
For registries that both sides append to, take the union of both sides' entries. These include
135+
`$covered` / `$coveredElsewhere` in `turbo-ext/tests/smoke.php`, `VENDORED_CLASS_MAP`, and
136+
`pt_class_refs` keys. MINIT registrations and the build files' source lists no longer exist: a
137+
replayed commit that adds a `pt_register_*()` call to `main.cpp`, its declaration to `support.h` or
138+
a `.cpp` name to `config.w32` drops that hunk and defines the function with
139+
`PT_MINIT_REGISTRATION(pt_register_*)` in its own file instead.
140+
141+
## 4. Replay
142+
143+
```bash
144+
GIT_SEQUENCE_EDITOR="php <skill dir>/rebase-todo.php" \
145+
REBASE_DROP="<shas to drop>" REBASE_EDIT="<shas to stop after>" \
146+
git rebase -i --onto "$NEW_BASE" "$OLD_BASE" "$BRANCH"
147+
```
148+
149+
The helper aborts the rebase before anything is replayed if a listed SHA has no pick line. Git's rerere
150+
is enabled on this machine. It may reapply a resolution recorded during an earlier attempt, so review
151+
those hunks (`git rerere diff`) like any other resolution.
152+
153+
At each stop:
154+
155+
- **Conflict.** Run `git diff --name-only --diff-filter=U` to list the conflicted files and
156+
`git show REBASE_HEAD` to see the commit being replayed. Resolve according to the plan, and make this
157+
commit's planned mirror edits now as well. A conflicting commit marked `edit` is committed by
158+
`--continue` without stopping again, so the edit stop never comes. Stage files by name, never a
159+
directory. Delete any `*.orig` or `*.rej` files. Then run `GIT_EDITOR=true git rebase --continue`.
160+
- **Planned `edit` stop.** Make the mirror edit, `git add <files>`, `git commit --amend --no-edit`, then
161+
`git rebase --continue`.
162+
- **After touching C++**: rebuild turbo-ext incrementally (`make -C turbo-ext`) and fix compile errors
163+
in the same commit. After touching PHP, run `php -l` on the files.
164+
- **A commit that unexpectedly became empty** means a resolution dropped its content. Investigate it;
165+
don't `--skip` it. Skip only commits the plan already marked as contained in the base.
166+
- If a stop reveals something the plan didn't foresee and the right resolution is unclear, stop and ask
167+
instead of guessing.
168+
169+
## 5. Check nothing was lost
170+
171+
```bash
172+
git range-diff "$OLD_BASE..$OLD_TIP" "$NEW_BASE..HEAD"
173+
```
174+
175+
Review every commit that range-diff reports as changed. Each difference must be an intended conflict
176+
resolution or mirror port.
177+
178+
```bash
179+
# files the rebase changed that the base never touched: exactly the adaptations and mirror ports
180+
comm -23 <(git diff --name-only "$OLD_TIP" HEAD | sort) <(git diff --name-only "$OLD_BASE" "$NEW_BASE" | sort)
181+
# base changes to files the feature never touched must arrive unchanged: expect no output other than
182+
# the planned mirror ports (xargs runs nothing on empty input - a bare `git diff --` would diff everything)
183+
comm -23 <(git diff --name-only "$OLD_BASE" "$NEW_BASE" | sort) <(git diff --name-only "$OLD_BASE" "$OLD_TIP" | sort) | xargs git diff --stat "$NEW_BASE" HEAD --
184+
```
185+
186+
If `git diff --quiet "$NEW_BASE" HEAD -- turbo-ext/src` reports a difference, run `make bump-turbo`. It
187+
creates the single bump commit, or amends the unpushed one.
188+
189+
## 6. Verify
190+
191+
1. Sync the dependencies, because the base may have changed the locks: run `composer install`,
192+
`composer install -d tests` and `composer dump-autoload`. The dump regenerates `vendor/attributes.php`
193+
and the turbo manifest. `composer install --dry-run` must then report nothing to install.
194+
2. Run `make phpstan` and `make tests`.
195+
3. If any turbo twin, turbo-ext file or php-parser version differs from `NEW_BASE`, run the verify list
196+
in `turbo-ext/CLAUDE.md` at HEAD:
197+
- the strict build,
198+
- `turbo-ext/tests/smoke.php` (ALL OK),
199+
- `turbo-ext/bin/side-by-side.php`,
200+
- `turbo-ext/tests/signature-parity.php`,
201+
- `turbo-ext/tests/walk-trace.php` where it exists,
202+
- `turbo-ext/tests/parser-corpus.php` when `turbo-ext/src/parser/` or php-parser changed,
203+
- `make lint-turbo` when C++ changed,
204+
- `make tests` with the extension loaded,
205+
- `--error-format=raw` analysis output that is byte-identical with the extension loaded and not
206+
loaded.
207+
4. Make sure the build you test is the one that's loaded. The global ini may load a `.so` from another
208+
worktree. Load this checkout's build through a scratch `PHP_INI_SCAN_DIR` (or
209+
`php -n -d extension=...`), and confirm that `phpversion('phpstan_turbo')` equals
210+
`EXPECTED_EXTENSION_VERSION`. On a mismatch, turbo silently stays off.
211+
212+
Fix a failure in the commit that introduced it: `git commit --fixup=<sha>`, then
213+
`GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash "$NEW_BASE"`, then `make bump-turbo` again. Don't add a
214+
trailing fix commit.
215+
216+
## 7. Report and push
217+
218+
Tell the user:
219+
220+
- the old base and the new base,
221+
- which commits were dropped, and why,
222+
- each conflict, with its file and how it was resolved,
223+
- each mirror port, with its class and commit,
224+
- the verification results,
225+
- the backup branch `backup/$BRANCH-pre-rebase`.
226+
227+
Push only when asked: `git push --force-with-lease="$BRANCH:$OLD_TIP" origin "$BRANCH"`. Delete the
228+
backup branch only after the user confirms the result.

0 commit comments

Comments
 (0)